當領域模型中的值對象(如 Address、Money、DateRange)需映射到單表多列,或需特殊序列化/加密/壓縮邏輯時,Hibernate 內建類型無法滿足。實現 UserType 或 CompositeUserType 可將領域概念無縫映射到關係表結構。
public interface UserType {
int[] sqlTypes(); // 映射的 SQL 類型碼(如 VARCHAR, VARCHAR)
Class returnedClass(); // 返回的 Java 類型
boolean equals(Object x, Object y); // 臟檢比較
int hashCode(Object x); // 雜湊碼(用於集合/快取)
Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner);
void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session);
Object deepCopy(Object value); // 深拷貝(不可變對象可直接返回)
boolean isMutable(); // 是否可變(false 可優化)
Serializable disassemble(Object value); // 二級快取序列化
Object assemble(Serializable cached, Object owner); // 反序列化
Object replace(Object original, Object target, Object owner); // 合併
}
@Value // Lombok 不可變值對象
public class Address {
private final String homeAddr;
private final String workAddr;
public Address(String homeAddr, String workAddr) {
this.homeAddr = homeAddr;
this.workAddr = workAddr;
}
// getter、equals/hashCode 自動生成
}
public class AddressType implements UserType {
private static final int[] SQL_TYPES = { Types.VARCHAR, Types.VARCHAR };
@Override public int[] sqlTypes() { return SQL_TYPES; }
@Override public Class returnedClass() { return Address.class; }
@Override public boolean equals(Object x, Object y) {
if (x == y) return true;
if (x == null || y == null) return false;
return x.equals(y); // 委託值對象自身 equals
}
@Override public int hashCode(Object x) { return x.hashCode(); }
@Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
String home = rs.getString(names[0]);
String work = rs.getString(names[1]);
if (home == null && work == null) return null;
return new Address(home, work);
}
@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);
} else {
Address addr = (Address) value;
st.setString(index, addr.getHomeAddr());
st.setString(index + 1, addr.getWorkAddr());
}
}
@Override public Object deepCopy(Object value) { return value; } // 不可變
@Override public boolean isMutable() { return false; }
@Override public Serializable disassemble(Object value) { return (Serializable) value; }
@Override public Object assemble(Serializable cached, Object owner) { return cached; }
@Override public Object replace(Object original, Object target, Object owner) { return original; }
}
<property name="address" type="com.example.type.AddressType">
<column name="home_addr"/>
<column name="work_addr"/>
</property>
// 或註解
@Type(type = "com.example.type.AddressType")
@Columns(columns = { @Column(name="home_addr"), @Column(name="work_addr") })
private Address address;
繼承 UserType 並新增:
String[] getPropertyNames():屬性名陣列(如 homeAddr, workAddr)Type[] getPropertyTypes():對應 Hibernate 類型Object getPropertyValue(Object component, int property):獲取屬性值void setPropertyValue(Object component, int property, Object value):設置屬性值(可變對象需)from User u where u.address.homeAddr = 'Beijing'public class AddressCompositeType implements CompositeUserType {
private static final String[] PROPERTY_NAMES = {"homeAddr", "workAddr"};
private static final Type[] PROPERTY_TYPES = { StandardBasicTypes.STRING, StandardBasicTypes.STRING };
@Override public String[] getPropertyNames() { return PROPERTY_NAMES; }
@Override public Type[] getPropertyTypes() { return PROPERTY_TYPES; }
@Override public Object getPropertyValue(Object component, int property) {
Address addr = (Address) component;
return property == 0 ? addr.getHomeAddr() : addr.getWorkAddr();
}
@Override public void setPropertyValue(Object component, int property, Object value) {
throw new UnsupportedOperationException("Address is immutable");
}
// UserType 方法同前,建議委託內部 UserType 實現或共用邏輯
}
public class EncryptedStringType implements UserType {
// nullSafeSet 寫入前 AES 加密
// nullSafeGet 讀取後 AES 解密
// sqlTypes 返回 VARBINARY 或 BLOB
}
@Type(type = "json")
@Column(columnDefinition = "jsonb")
private Map metadata;
public class StatusEnumType implements UserType {
// 存儲 code (Integer) 而非 name
// returnedClass = OrderStatus.class
}
disassemble/assemble 返回可序列化形式@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)isMutable=false 可大幅提升快取效能| 問題 | 原因 | 解決 |
|---|---|---|
| NullPointerException in nullSafeGet | 未處理 rs.wasNull() 或列為 NULL | 先判斷所有列是否為 null 再構造對象 |
| 臟檢失效、重複 UPDATE | equals/hashCode 未正確實現、可變對象未深拷貝 | 值對象不可變、正确实現 equals/hashCode、isMutable=false |
| HQL 查詢屬性報錯 | 使用 UserType 而非 CompositeUserType | 需屬性級查詢時改用 CompositeUserType |
| Schema 生成錯誤 | sqlTypes 長度與映射列數不匹配 | 嚴格對應:N 個列 = N 個 sqlTypes |
| 二級快取序列化失敗 | disassemble 返回非 Serializable | 確保返回實現 Serializable 的對象或 DTO |
@Converter(autoApply = false)
public class AddressConverter implements AttributeConverter {
@Override public String convertToDatabaseColumn(Address attr) {
return attr.getHomeAddr() + "|" + attr.getWorkAddr(); // 單列 JSON/分隔符
}
@Override public Address convertToEntityAttribute(String dbData) {
String[] parts = dbData.split("\\|");
return new Address(parts[0], parts[1]);
}
}
// 使用
@Convert(converter = AddressConverter.class)
@Column(name = "address_info")
private Address address;
優勢:API 更簡潔、標準化、單列存儲。劣勢:無法映射多列、無法 HQL 查詢內部屬性。
UserType/CompositeUserType 是 Hibernate 映射擴展的核心擴展點。掌握其契約、正確處理不可變性與臟檢、根據查詢需求選擇 UserType(多列透明)或 CompositeUserType(屬性可查詢),能讓領域模型優雅落地關係表。現代項目中,簡單單列轉換優先 AttributeConverter,複雜多列映射堅定選擇 CompositeUserType。