首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> MySQL 資料庫 >> MySQL 綜合教學

Hibernate 自定義類型實戰:UserType 與 CompositeUserType 深度解析

     編輯:MySQL 綜合教學

為什麼需要自定義類型?

當領域模型中的值對象(如 Address、Money、DateRange)需映射到單表多列,或需特殊序列化/加密/壓縮邏輯時,Hibernate 內建類型無法滿足。實現 UserType 或 CompositeUserType 可將領域概念無縫映射到關係表結構。

UserType 介面核心契約

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); // 合併
}

實戰案例:Address 值對象映射到 home_addr、work_addr 兩列

領域模型

@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 自動生成
}

UserType 實現

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; }
}

映射配置 (XML / Annotation)

<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;

CompositeUserType:更豐富的查詢與組合能力

繼承 UserType 並新增:

  • String[] getPropertyNames():屬性名陣列(如 homeAddr, workAddr)
  • Type[] getPropertyTypes():對應 Hibernate 類型
  • Object getPropertyValue(Object component, int property):獲取屬性值
  • void setPropertyValue(Object component, int property, Object value):設置屬性值(可變對象需)

關鍵優勢

  1. HQL/JPQL 直接查詢屬性:from User u where u.address.homeAddr = 'Beijing'
  2. Criteria API 支援:動態構建條件
  3. 元資料暴露:工具可識別內部結構

CompositeUserType 實現骨架

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 實現或共用邏輯
}

進階場景與最佳實踐

1. 加密敏感字段

public class EncryptedStringType implements UserType {
    // nullSafeSet 寫入前 AES 加密
    // nullSafeGet 讀取後 AES 解密
    // sqlTypes 返回 VARBINARY 或 BLOB
}

2. JSON 文檔映射 (MySQL 5.7+/PG JSONB)

@Type(type = "json")
@Column(columnDefinition = "jsonb")
private Map metadata;

3. 枚舉自定義存儲

public class StatusEnumType implements UserType {
    // 存儲 code (Integer) 而非 name
    // returnedClass = OrderStatus.class
}

4. 二級快取整合

  • 實現 disassemble/assemble 返回可序列化形式
  • 註解 @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
  • 不可變對象 isMutable=false 可大幅提升快取效能

常見坑點與避雷指南

問題原因解決
NullPointerException in nullSafeGet未處理 rs.wasNull() 或列為 NULL先判斷所有列是否為 null 再構造對象
臟檢失效、重複 UPDATEequals/hashCode 未正確實現、可變對象未深拷貝值對象不可變、正确实現 equals/hashCode、isMutable=false
HQL 查詢屬性報錯使用 UserType 而非 CompositeUserType需屬性級查詢時改用 CompositeUserType
Schema 生成錯誤sqlTypes 長度與映射列數不匹配嚴格對應:N 個列 = N 個 sqlTypes
二級快取序列化失敗disassemble 返回非 Serializable確保返回實現 Serializable 的對象或 DTO

現代替代方案:JPA 2.1 AttributeConverter

@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。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved