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

如何将Hibernate嵌入式类映射为多字段自定义Type并关联java.sql.Types?

解决Hibernate复合类型作为CustomVersionType的映射问题

首先,咱们先拆解你遇到的MappingException错误根源:你在实体类里同时混用了@Type、@Embedded和@Version注解,这三者的作用是冲突的——@Embedded是让Hibernate把嵌入式类的字段直接展开到主实体表中,而@Type是指定单个自定义类型对应单个数据库列,这就导致Hibernate预期这个类型对应1列,但实际你的TestType需要3列,从而抛出字段数不匹配的异常。

要实现把复合类型映射到3个数据库字段并作为CustomVersionType使用,正确的思路是实现Hibernate的CompositeUserType接口(专门处理多列映射的自定义类型),而不是依赖@Embeddable。下面是具体的实现步骤和代码示例:

1. 重构TestType类

去掉@Embeddable注解,让它成为一个普通的可序列化类,并实现Comparable接口(因为@Version需要版本类型支持比较逻辑,用于乐观锁检查):

import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;

public class TestType implements Serializable, Comparable<TestType> {
    private String id1 = UUID.randomUUID().toString();
    private String id2;
    private String id3;

    // Getters & Setters
    public String getId1() { return id1; }
    public void setId1(String id1) { this.id1 = id1; }
    public String getId2() { return id2; }
    public void setId2(String id2) { this.id2 = id2; }
    public String getId3() { return id3; }
    public void setId3(String id3) { this.id3 = id3; }

    // 版本比较逻辑,需要覆盖所有字段确保准确性
    @Override
    public int compareTo(TestType o) {
        int id1Compare = this.id1.compareTo(o.id1);
        if (id1Compare != 0) return id1Compare;
        int id2Compare = this.id2.compareTo(o.id2);
        if (id2Compare != 0) return id2Compare;
        return this.id3.compareTo(o.id3);
    }

    // 重写equals和hashCode,确保Hibernate能正确识别对象相等性
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TestType testType = (TestType) o;
        return Objects.equals(id1, testType.id1) &&
               Objects.equals(id2, testType.id2) &&
               Objects.equals(id3, testType.id3);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id1, id2, id3);
    }
}

2. 实现CompositeUserType接口

这个接口是Hibernate专门为多列自定义类型设计的,需要实现数据的读取、写入、属性映射等核心方法:

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.type.StringType;
import org.hibernate.type.Type;
import org.hibernate.usertype.CompositeUserType;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Objects;

public class TestTypeCompositeUserType implements CompositeUserType {

    // 返回TestType的属性名,对应数据库列的映射
    @Override
    public String[] getPropertyNames() {
        return new String[]{"id1", "id2", "id3"};
    }

    // 返回每个属性对应的Hibernate内置类型
    @Override
    public Type[] getPropertyTypes() {
        return new Type[]{StringType.INSTANCE, StringType.INSTANCE, StringType.INSTANCE};
    }

    // 获取指定索引的属性值
    @Override
    public Object getPropertyValue(Object component, int property) throws HibernateException {
        TestType testType = (TestType) component;
        return switch (property) {
            case 0 -> testType.getId1();
            case 1 -> testType.getId2();
            case 2 -> testType.getId3();
            default -> throw new IllegalArgumentException("无效的属性索引: " + property);
        };
    }

    // 设置指定索引的属性值
    @Override
    public void setPropertyValue(Object component, int property, Object value) throws HibernateException {
        TestType testType = (TestType) component;
        switch (property) {
            case 0 -> testType.setId1((String) value);
            case 1 -> testType.setId2((String) value);
            case 2 -> testType.setId3((String) value);
            default -> throw new IllegalArgumentException("无效的属性索引: " + property);
        }
    }

    @Override
    public Class returnedClass() {
        return TestType.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y) return true;
        if (x == null || y == null) return false;
        return x.equals(y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    // 从ResultSet读取数据并组装成TestType对象
    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException {
        String id1 = rs.getString(names[0]);
        String id2 = rs.getString(names[1]);
        String id3 = rs.getString(names[2]);
        if (rs.wasNull()) return null;
        TestType testType = new TestType();
        testType.setId1(id1);
        testType.setId2(id2);
        testType.setId3(id3);
        return testType;
    }

    // 将TestType对象写入PreparedStatement
    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException {
        if (value == null) {
            st.setNull(index, Types.VARCHAR);
            st.setNull(index+1, Types.VARCHAR);
            st.setNull(index+2, Types.VARCHAR);
            return;
        }
        TestType testType = (TestType) value;
        st.setString(index, testType.getId1());
        st.setString(index+1, testType.getId2());
        st.setString(index+2, testType.getId3());
    }

    // 深度复制对象,确保Hibernate的状态管理正确
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        if (value == null) return null;
        TestType original = (TestType) value;
        TestType copy = new TestType();
        copy.setId1(original.getId1());
        copy.setId2(original.getId2());
        copy.setId3(original.getId3());
        return copy;
    }

    @Override
    public boolean isMutable() {
        return true; // TestType的属性可修改,标记为可变
    }

    @Override
    public Serializable disassemble(Object value, SharedSessionContractImplementor session) throws HibernateException {
        return (Serializable) deepCopy(value);
    }

    @Override
    public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException {
        return deepCopy(cached);
    }

    @Override
    public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner) throws HibernateException {
        return deepCopy(original);
    }
}

3. 修改实体类映射

去掉@Embedded和@AttributeOverrides,改用@Type指定自定义的CompositeUserType,并通过@Columns(复数形式)指定每个属性对应的数据库列:

import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name="`hib_test`")
public class HibernateTest implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @SequenceGenerator(name="seq-gen", sequenceName="seq", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq-gen")
    @Column(name="`id`", nullable=false)
    private Long id;

    @Column(name="`NAME`", length=100)
    private String name;

    @Version
    @Type(type = "com.proto.usertypes.TestTypeCompositeUserType")
    @Columns(columns = {
            @Column(name="`id1_in_testtype`", nullable=false, length=100),
            @Column(name="`id2_in_testtype`", nullable=false, length=100),
            @Column(name="`id3_in_testtype`", nullable=false, length=100)
    })
    private TestType testType;

    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public TestType getTestType() { return testType; }
    public void setTestType(TestType testType) { this.testType = testType; }
}

4. 关于java.sql.Types访问的说明

你的需求是让这个复合类型能通过java.sql.Types访问,其实在CompositeUserType的nullSafeSet方法中,我们已经直接使用了Types.VARCHAR来指定每个列的SQL类型。如果需要让Hibernate在生成DDL或处理类型识别时更顺畅,可以扩展数据库方言并注册自定义类型:

import org.hibernate.dialect.PostgreSQLDialect;

public class CustomPostgreSQLDialect extends PostgreSQLDialect {
    public CustomPostgreSQLDialect() {
        super();
        // 注册自定义类型与SQL类型的映射
        registerHibernateType(Types.VARCHAR, TestTypeCompositeUserType.class.getName());
    }
}

然后在Hibernate配置文件中指定使用这个自定义方言即可。

总结

这种方案是完全可行的:通过CompositeUserType可以将复合类型映射到多个数据库字段,同时结合@Version实现乐观锁的版本检查。需要注意的是,自定义类型必须正确实现equals、hashCode、deepCopy等方法,确保Hibernate能正确处理对象的状态管理和比较逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 07:32:12