如何将Hibernate嵌入式类映射为多字段自定义Type并关联java.sql.Types?
首先,咱们先拆解你遇到的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

