Issue Summary
问题摘要
I have a custom type that I use in a project with Spring Boot 2/Hibernate 5. I am attempting to migrate this project to Spring Boot 3/Hibernate 6 and I cannot seem to get this custom type to work. This type is a wrapper around a UUID, and should be treated as a UUID when reading/writing from/to a database.
我有一个定制类型,我在一个带有Spring Boot 2/Hibernate5的项目中使用。我试图将这个项目迁移到Spring Boot3/Hibernate6,但我似乎无法让这个定制类型工作。此类型是UUID的包装器,在从数据库读取/写入数据库时应将其视为UUID。
Existing Code
现有代码
My TypedId
class allows for distinguishing different UUIDs based on the entity they are associated with. It's a relatively simple class:
我的TypeId类允许根据不同的UUID关联的实体来区分它们。这是一个相对简单的类:
data class TypedId<T>(val uuid: UUID = UUID.randomUUID()) : Serializable, Comparable<TypedId<T>> {
constructor(id: String) : this(UUID.fromString(id))
override fun compareTo(other: TypedId<T>): Int = this.uuid.compareTo(other.uuid)
override fun toString(): String = uuid.toString()
}
This is configured in a base DatabaseRecord abstract class that all my JPA Entities extend:
这是在我的所有JPA实体都扩展的基础DatabaseRecord抽象类中配置的:
@MappedSuperclass
@TypeDef(defaultForType = TypedId::class, typeClass = TypedIdJpaType::class)
abstract class DatabaseRecord<T> : Persistable<TypedId<T>> {
@Id var uid: TypedId<T> = TypedId()
@Transient private var innerIsNew: Boolean = true
override fun getId(): TypedId<T> = uid
override fun isNew(): Boolean = innerIsNew
private fun handleIsNew() {
innerIsNew = false
}
@PrePersist
open fun onPrePersist() {
handleIsNew()
}
@PostLoad
open fun onPostLoad() {
handleIsNew()
}
}
The important part of the above code is the @TypeDef
. That points to the JPA Type class that configures the entire type definition. Here is the relevant code pulled in by that annotation:
上述代码的重要部分是@TypeDef。它指向配置整个类型定义的JPA Type类。下面是该注释引入的相关代码:
class TypedIdJpaType :
AbstractSingleColumnStandardBasicType<TypedId<*>>(
PostgresUUIDSqlTypeDescriptor.INSTANCE, TypedIdDescriptor.INSTANCE) {
override fun getName(): String = TypedId::class.java.simpleName
override fun registerUnderJavaType(): Boolean = true
}
class TypedIdDescriptor : AbstractTypeDescriptor<TypedId<*>>(TypedId::class.java) {
companion object {
val INSTANCE = TypedIdDescriptor()
}
override fun fromString(string: String): TypedId<*> = TypedId<Any>(string)
override fun <X : Any> wrap(value: X?, options: WrapperOptions): TypedId<*>? =
value?.let { nonNullValue ->
when (nonNullValue) {
is ByteArray ->
TypedId(UUIDTypeDescriptor.ToBytesTransformer.INSTANCE.parse(nonNullValue))
is String ->
TypedId<Any>(UUIDTypeDescriptor.ToStringTransformer.INSTANCE.parse(nonNullValue))
is UUID -> TypedId<Any>(nonNullValue)
else -> throw unknownWrap(nonNullValue::class.java)
}
}
override fun <X : Any> unwrap(value: TypedId<*>, type: Class<X>, options: WrapperOptions): X =
UUIDTypeDescriptor.INSTANCE.unwrap(value.uuid, type, options)
}
Lastly, here is my sample entity that I have created for very basic test cases involving all of this code:
最后,下面是我为涉及所有代码的非常基本的测试用例创建的样例实体:
interface CountryId
@Entity
@Table(name = "countries")
class Country(var name: String = "") : DatabaseRecord<CountryId>()
The Core Problem
核心问题
With Hibernate 6, @TypeDef
, TypeDescriptor
, etc are all removed. This means the entire mechanism for converting the TypedId
no longer works. I've been trying to identify an alternate solution.
在Hibernate6中,@TypeDef、TypeDescriptor等都被删除了。这意味着转换TypeID的整个机制不再起作用。我一直在努力寻找另一个解决方案。
The Question
问题是
I've tried a Converter
. I've tried implementing AbstractStandardBasicType
. I'm just very lost right now.
我试过转换器。我已经尝试实现AbstractStandardBasicType。我只是现在很迷茫。
I've been reading over the new Hibernate 6 user guide, but nothing I've gleaned from there has helped yet.
我一直在阅读新的Hibernate6用户指南,但我从那里收集到的任何东西都没有帮助。
Additional Details
其他详细信息
After posting this question, I realized the error message should be useful. This happens when I try to use a Spring JpaRepository
to save (aka insert) the above entity:
在发布了这个问题后,我意识到错误消息应该是有用的。当我尝试使用Spring JpaRepository保存(也称为插入)上面的实体时,就会发生这种情况:
could not execute statement [ERROR: column "uid" is of type uuid but expression is of type bytea
Hint: You will need to rewrite or cast the expression.
更多回答
优秀答案推荐
Wow, this was quite the rabbit hole. So, a few quick findings for the group. It seems that Hibernate is moving away from the AbstractSingleColumnStandardBasicType
approach for type definitions. Even after I got it implemented properly, registering it was a nightmare. The primary issue with registering it was that every option that still exists in Hibernate 6 seems to be marked for deprecation. Basically that whole approach was a dead-end.
哇,这可真是个有趣的兔子洞。因此,为该小组提供一些快速的发现。对于类型定义,Hibernate似乎正在远离AbstractSingleColumnStandardBasicType方法。即使在我正确地实现它之后,注册它也是一场噩梦。注册它的主要问题是,Hibernate6中仍然存在的每个选项似乎都被标记为弃用。基本上,整个方法都是死胡同。
The AttributeConverter
approach also doesn't seem to work for more complex types like my TypedId
. I could never get Hibernate to recognize it.
AttributeConverter方法似乎也不适用于像我的TypeID这样的更复杂的类型。我永远也不能让Hibernate识别它。
In the end, I discovered that using the @JavaType
and @JdbcType
mechanisms Hibernate 6 offers in tandem was the solution.
最后,我发现解决方案是同时使用Hibernate6提供的@JavaType和@JdbcType机制。
First, I needed to write a JavaType
implementation, which was easy since my type is just a wrapper around UUID
:
首先,我需要编写一个JavaType实现,这很简单,因为我的类型只是uuid的包装器:
class TypedIdJavaType:
AbstractClassJavaType<TypedId<*>>(TypedId::class.java) {
override fun <X : Any?> unwrap(value: TypedId<*>?, type: Class<X>, options: WrapperOptions?): X =
UUIDJavaType.INSTANCE.unwrap(value?.uuid, type, options)
override fun <X : Any?> wrap(value: X, options: WrapperOptions?): TypedId<*> =
TypedId<Any>(UUIDJavaType.INSTANCE.wrap(value, options))
}
Then, I reference my new JavaType
via the annotation on my uid
property. However, I also need to include the @JdbcType(UUIDJdbcType::class)
as well. See below:
然后,我通过uid属性上的注释引用新的JavaType。但是,我还需要包括@JdbcType(UUIDJdbcType::Class)。请参见下面的内容:
@MappedSuperclass
@TypeDef(defaultForType = TypedId::class, typeClass = TypedIdJpaType::class)
abstract class DatabaseRecord<T> : Persistable<TypedId<T>> {
@Id
@JavaType(TypedIdJavaType::class)
@JdbcType(UUIDJdbcType::class)
var uid: TypedId<T> = TypedId()
@Transient private var innerIsNew: Boolean = true
override fun getId(): TypedId<T> = uid
override fun isNew(): Boolean = innerIsNew
private fun handleIsNew() {
innerIsNew = false
}
@PrePersist
open fun onPrePersist() {
handleIsNew()
}
@PostLoad
open fun onPostLoad() {
handleIsNew()
}
}
The reason for needing both appears to be that @JdbcType
tells Hibernate what this field is supposed to be when it is first scanning the entity. Once it knows what type it is supposed to be via @JdbcType
, it then checks the other annotations, Hibernate's configuration, etc to find an available mechanism for converting the provided type to the JDBC Type.
同时需要这两个字段的原因似乎是,@JdbcType在Hibernate第一次扫描实体时告诉它这个字段应该是什么。一旦它通过@JdbcType知道它应该是什么类型,它就会检查其他注释、Hibernate的配置等,以找到将提供的类型转换为JDBC类型的可用机制。
And that's it. This was a massive rabbit hole that IMO is not well documented at the moment.
就是这样。这是一个巨大的兔子洞,而国际海事组织目前还没有很好的记录。
As mentioned by craigmiller160, the documentation for custom user types in Hibernate 6 is not well documented. Hopefully this improves in the future.
正如Craigmiler160所提到的,Hibernate6中的定制用户类型的文档没有很好的文档记录。希望这一点在未来有所改善。
I had a similar setup (using custom domain object as entity ID) but in addition was using an IdentifierGenerator:
我有一个类似的设置(使用自定义域对象作为实体ID),但另外还使用了一个IdentifierGenerator:
public class UserAccountIdGenerator implements IdentifierGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object)
throws HibernateException {
return new UserAccountId(UUID.randomUUID());
}
}
Note that the type of the return value has to be of the domain object. Otherwise Hibernate throws an (unhelpful) error like so:
请注意,返回值的类型必须为域对象。否则,Hibernate会抛出(无用的)错误,如下所示:
org.springframework.orm.jpa.JpaSystemException: Could not set value of type [java.lang.String] : `com.mtc.app.UserAccount.id` (setter)
Caused by: org.hibernate.PropertyAccessException: Could not set value of type [java.lang.String] : `com.mtc.app.UserAccount.id` (setter)
Caused by: java.lang.IllegalArgumentException: Can not set com.mtc.app.UserAccountId field com.mtc.app.UserAccount.id to java.lang.String
更多回答
我是一名优秀的程序员,十分优秀!