當 Hibernate 內建型別(String、Integer、Date 等)無法滿足物件模型與資料表結構不匹配時,需實作 org.hibernate.usertype.UserType 介面,自定義Java 物件 ⇄ 資料庫欄位的序列化/反序列化邏輯。
| 方法 | 職責 | 典型實作 |
|---|---|---|
| sqlTypes() | 返回資料庫欄位 SQL 型別碼(java.sql.Types) | new int[]{Types.VARCHAR} |
| returnedClass() | 返回 Java 端型別 | List.class |
| nullSafeGet(ResultSet, names, owner) | 從 ResultSet 讀取並轉為 Java 物件 | 解析字串為 List |
| nullSafeSet(PreparedStatement, value, index) | 將 Java 物件寫入 PreparedStatement | 序列化 List 為字串 |
| deepCopy(value) | 返回深拷貝,避免共享可變物件 | new ArrayList<>(list) |
| isMutable() | 型別是否可變,影響快取策略 | true(集合通常可變) |
| equals(x, y) / hashCode(x) | 持久化狀態的相等性判斷 | 比較集合內容 |
| disassemble/assemble | 二級快取序列化/反序列化 | 可委託 deepCopy |
| replace(original, target, owner) | 合併脫管物件時替換值 | 返回 deepCopy(original) |
需求:User 實體含 List,資料表單欄 emails VARCHAR 以分號 ; 分隔儲存。
public class EmailListType implements UserType {
private static final String DELIMITER = ";";
private static final int[] SQL_TYPES = {Types.VARCHAR};
@Override public int[] sqlTypes() { return SQL_TYPES; }
@Override public Class returnedClass() { return List.class; }
@Override public boolean isMutable() { return true; }
// 反序列化:資料庫字串 → List
@Override
public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
throws HibernateException, SQLException {
String val = (String) Hibernate.STRING.nullSafeGet(rs, names[0]);
if (val == null || val.isEmpty()) return Collections.emptyList();
return Arrays.asList(val.split(DELIMITER));
}
// 序列化:List → 資料庫字串
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index)
throws HibernateException, SQLException {
if (value == null) {
Hibernate.STRING.nullSafeSet(st, null, index);
return;
}
List list = (List) value;
String joined = String.join(DELIMITER, list);
Hibernate.STRING.nullSafeSet(st, joined, index);
}
@Override public Object deepCopy(Object value) {
if (value == null) return null;
return new ArrayList<>((List>) value);
}
@Override public boolean equals(Object x, Object y) {
if (x == y) return true;
if (x == null || y == null) return false;
return ((List>) x).equals((List>) y);
}
@Override public int hashCode(Object x) {
return x == null ? 0 : x.hashCode();
}
@Override public Serializable disassemble(Object value) {
return (Serializable) deepCopy(value);
}
@Override public Object assemble(Serializable cached, Object owner) {
return deepCopy(cached);
}
@Override public Object replace(Object original, Object target, Object owner) {
return deepCopy(original);
}
}
<class name="User" table="t_user">
<id name="id" column="user_id"><generator class="native"/></id>
<property name="name" column="user_name"/>
<!-- 指定自定義類型全限定名 -->
<property name="emails" column="emails" type="com.example.type.EmailListType"/>
</class>
User user = new User();
user.setName("shun");
user.setEmails(Arrays.asList("a@b.com", "c@d.com"));
session.save(user); // 執行 INSERT,emails 欄位存 "a@b.com;c@d.com"
User loaded = session.get(User.class, user.getId());
System.out.println(loaded.getEmails()); // [a@b.com, c@d.com]
現象:儲存單個 email 時,資料庫讀回時發生 ArrayList cannot be cast to String。
原因:Hibernate 內部機制在某些版本下,單元素集合可能被雙重包裝。
修正:在 nullSafeGet 中加強防禦:
List result = new ArrayList<>();
if (val != null) {
for (String s : val.split(DELIMITER)) {
if (!s.isEmpty()) result.add(s);
}
}
return result;
若需將單一 Java 物件映射為多個資料庫欄位(如 Address → street, city, zip),實作 CompositeUserType,需額外實作:
getPropertyNames()、getPropertyTypes()getPropertyValue(object, i)、setPropertyValue(object, i, value)適用於值物件拆分映射場景。
XxxType,放在 type 包下。nullSafeGet/Set 委託對應基本型別(Hibernate.STRING 等)處理 null。deepCopy 必須返回新實例,確保實體間狀態隔離。isMutable() 誠實標註,影響二級快取行為。@Convert / AttributeConverter(Hibernate 5+),更標準、可移植。