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

多模块Spring项目Hibernate XML映射Kotlin不可变类报错求助

解决Hibernate XML映射外部不可变复合ID类的构造器找不到问题

问题场景

我维护一个多模块Spring项目,部分类属于外部依赖(无法修改源码),必须通过Hibernate XML映射为实体。示例实体和复合ID类采用Kotlin data class定义:

data class GeoLocationEntity(
    val id: GeoLocationId,
)

data class GeoLocationId(
    val id: String,
    val timestamp: Instant,
)

对应的Hibernate XML映射文件如下:

<?xml version = "1.0" encoding = "utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class entity-name="GeoLocationEntity" name="com.core.api.GeoLocationEntity"
           table="geo_location" mutable="false">
        <composite-id name="id" class="com.core.api.GeoLocationId" access="field">
            <key-property column="id" name="id"/>
            <key-property column="timestamp" name="timestamp"/>
        </composite-id>
    </class>
</hibernate-mapping>

运行时抛出异常:

org.hibernate.InstantiationException: Unable to locate constructor for embeddable 'com.core.api.GeoLocationId'
    at org.hibernate.metamodel.internal.EmbeddableInstantiatorPojoStandard.instantiate(EmbeddableInstantiatorPojoStandard.java:58)
    at org.hibernate.type.ComponentType.deepCopy(ComponentType.java:483)
    at org.hibernate.event.internal.DefaultMergeEventListener.copyCompositeTypeId(DefaultMergeEventListener.java:246)
    ...(省略后续栈跟踪)

注:相同类用注解映射可正常运行,尝试替换为Java Record也仅能作为DTO使用,无法作为实体映射,求可行解决方案。

可行解决方案

1. 给Kotlin Data Class生成无参构造器(适用于可添加编译注解的场景)

Hibernate XML映射默认会尝试调用类的无参构造器,但Kotlin data class默认只生成带全参数的构造器。可以通过Kotlin的noarg编译插件为目标类生成无参构造器:

  • 在build.gradle.kts中引入插件:
plugins {
    kotlin("plugin.noarg") version "与项目匹配的Kotlin版本"
}

noArg {
    // 指定要生成无参构造器的注解,给GeoLocationId添加该注解
    annotation("javax.persistence.Embeddable")
}

然后给外部类GeoLocationId添加@Embeddable注解(如果有权限修改外部类源码),或者自定义一个注解并配置插件,通过编译时增强生成无参构造器。

2. 自定义Embeddable实例化器(适用于完全无法修改外部类的场景)

通过实现Hibernate的EmbeddableInstantiator接口,手动控制复合ID类的实例化逻辑:

  • 编写自定义实例化器:
public class GeoLocationIdInstantiator implements EmbeddableInstantiator {
    @Override
    public Object instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactory) {
        // 按XML映射中key-property的顺序获取参数
        String id = valueAccess.getValue(0, String.class);
        Instant timestamp = valueAccess.getValue(1, Instant.class);
        return new GeoLocationId(id, timestamp);
    }

    @Override
    public boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) {
        return object instanceof GeoLocationId;
    }

    @Override
    public boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) {
        return object.getClass().equals(GeoLocationId.class);
    }
}
  • 在XML映射中指定该实例化器:
<composite-id name="id" class="com.core.api.GeoLocationId" access="field">
    <meta attribute="instantiator">com.your.project.GeoLocationIdInstantiator</meta>
    <key-property column="id" name="id"/>
    <key-property column="timestamp" name="timestamp"/>
</composite-id>

3. 升级到Hibernate 6.x版本

Hibernate 6对Kotlin data class和Java Record的兼容性大幅提升,尤其是在XML映射场景下,它能自动识别类的带参构造器并完成实例化,无需额外配置。如果项目依赖允许,升级Hibernate版本是最省心的方案。

4. 本地实体类包装外部类

如果以上方法都无法生效,可以创建本地实体类映射到数据库表,在本地类和外部类之间做转换:

@Entity
@Table(name = "geo_location")
class LocalGeoLocationEntity(
    @EmbeddedId
    val id: LocalGeoLocationId
) {
    // 转换为外部类
    fun toExternal(): GeoLocationEntity {
        return GeoLocationEntity(GeoLocationId(id.id, id.timestamp))
    }

    companion object {
        // 从外部类转换为本地实体
        fun fromExternal(external: GeoLocationEntity): LocalGeoLocationEntity {
            return LocalGeoLocationEntity(LocalGeoLocationId(external.id.id, external.id.timestamp))
        }
    }
}

@Embeddable
data class LocalGeoLocationId(
    val id: String,
    val timestamp: Instant
)

Repository层操作本地实体类,业务层负责与外部类的转换,隔离Hibernate映射与外部类的依赖。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:50:06