You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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接口并注册:

  1. 定义类型实现:
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"
}
  1. 注册类型:
    创建配置类,使用@BasicTypeRegistration注解注册自定义类型:
import org.hibernate.annotations.BasicTypeRegistration
import org.springframework.context.annotation.Configuration

@Configuration
@BasicTypeRegistration(
    baseType = TypedId::class,
    typeClass = TypedIdBasicType::class
)
class HibernateTypeConfig
  1. 修改DatabaseRecord基类,移除@TypeDef即可,Hibernate会自动识别并使用注册的类型。

关键注意事项

  • 方案一的Converter更简洁,符合JPA标准,优先推荐。
  • 若使用PostgreSQL,确保数据库列类型为uuid,无需额外转换。
  • 测试时注意实体的isNew逻辑是否正常,避免影响持久化状态判断。

内容的提问来源于stack exchange,提问作者craigmiller160

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 18:34:56