Spring Boot3/Hibernate6中如何适配UUID包装类TypedId自定义类型?
问题概述
在Spring Boot 2/Hibernate 5项目中使用了基于UUID包装的自定义类型TypedId,迁移至Spring Boot 3/Hibernate 6后该类型无法正常工作,该类型读写数据库时需被视为UUID处理。
现有代码
TypedId类
用于区分不同实体关联的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() }
实体基类DatabaseRecord
所有JPA实体继承的抽象类,原通过@TypeDef配置类型:
@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() } }
原类型实现类
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) }
测试实体类
interface CountryId @Entity @Table(name = "countries") class Country(var name: String = "") : DatabaseRecord<CountryId>()
核心问题
Hibernate 6移除了@TypeDef、TypeDescriptor等API,导致原TypedId转换机制失效。
问题咨询
尝试使用Converter、实现AbstractStandardBasicType均未解决,请求适配Hibernate 6的解决方案。
报错信息
使用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.
翻译:
无法执行语句 [错误:列"uid"类型为uuid,但表达式类型为bytea
提示:你需要重写表达式或进行类型转换。]
解决方案
方案一:使用JPA Converter(推荐)
Hibernate 6对JPA Converter的支持更完善,定义双向Converter:
import jakarta.persistence.AttributeConverter import jakarta.persistence.Converter @Converter(autoApply = true) class TypedIdConverter : AttributeConverter<TypedId<*>, UUID> { override fun convertToDatabaseColumn(attribute: TypedId<*>?): UUID? { return attribute?.uuid } override fun convertToEntityAttribute(dbData: UUID?): TypedId<*>? { return dbData?.let { TypedId<Any>(it) } } }
修改DatabaseRecord基类,移除@TypeDef,并为uid字段添加@Convert注解(autoApply = true时可省略,显式指定更清晰):
@MappedSuperclass abstract class DatabaseRecord<T> : Persistable<TypedId<T>> { @Id @Convert(converter = TypedIdConverter::class) var uid: TypedId<T> = TypedId() // 其余代码不变... }
方案二:自定义Hibernate 6 BasicType
如果需要更底层的控制,可实现Hibernate 6的BasicType接口并注册:
- 定义类型实现:
import org.hibernate.type.AbstractSingleColumnStandardBasicType import org.hibernate.type.SqlTypes import org.hibernate.type.descriptor.java.UUIDJavaType import org.hibernate.type.descriptor.jdbc.PostgresUUIDJdbcType class TypedIdBasicType : AbstractSingleColumnStandardBasicType<TypedId<*>>( PostgresUUIDJdbcType.INSTANCE, object : UUIDJavaType() { override fun wrap(value: Any?, options: WrapperOptions): TypedId<*>? { val uuid = super.wrap(value, options) return uuid?.let { TypedId<Any>(it) } } override fun unwrap(value: TypedId<*>?, type: Class<*>, options: WrapperOptions): Any? { return value?.uuid?.let { super.unwrap(it, type, options) } } } ) { override fun getName(): String = "typed-id" }
- 注册类型:
创建配置类,使用@BasicTypeRegistration注解注册自定义类型:
import org.hibernate.annotations.BasicTypeRegistration import org.springframework.context.annotation.Configuration @Configuration @BasicTypeRegistration( baseType = TypedId::class, typeClass = TypedIdBasicType::class ) class HibernateTypeConfig
- 修改
DatabaseRecord基类,移除@TypeDef即可,Hibernate会自动识别并使用注册的类型。
关键注意事项
- 方案一的Converter更简洁,符合JPA标准,优先推荐。
- 若使用PostgreSQL,确保数据库列类型为
uuid,无需额外转换。 - 测试时注意实体的
isNew逻辑是否正常,避免影响持久化状态判断。
内容的提问来源于stack exchange,提问作者craigmiller160
相关产品推荐
相关产品推荐

