在企業級 Java 應用開發中,將物件持久化至關聯式資料庫是核心任務之一。無論採用手寫 SQL、JDBC,還是成熟的 ORM 框架(如 Hibernate),物件標識符的管理始終是個棘手難題。本文深入探討標識符管理的常見陷阱,並提出一種將 ID 生成職責從框架收回、於物件實例化即賦予 ID 的優雅解決方案。
在 JVM 記憶體中,物件由直接引用定位,虛擬機內部為每個物件分配 8 位元組的內部 ID。然而,一旦物件需持久化至資料庫,情況便大不相同。假設建立一個 Person 物件 person1 並存入資料庫,另一段程式碼讀取同一筆資料實例化為 person2。此時記憶體中存在兩個不同實例,卻對應資料庫同一列。若無適當的 equals() 與 hashCode() 實作,Set、Map 等集合將無法正確識別它們代表同一實體,導致資料重複或遺失。
最直觀的做法是使用資料庫主鍵 id 欄位實作 equals() 與 hashCode()。但此法存在嚴重缺陷:
id 均為 null,導致它們被視為相等,放入 Set 時僅保留一個。HashSet 後,若持久化導致 id 從 null 變為實際值,hashCode() 改變會使集合「丟失」該物件,後續 contains() 返回 false。Hibernate 官方文檔曾建議使用「業務鍵」—— 業務上唯一且相對穩定的屬性組合。但並非所有實體都有天然業務鍵,強行選用可變欄位極易埋下隱患。
問題根源在於「標識符生成權」掌握在 ORM 框架手中。解決思路極簡:在物件實例化瞬間,由應用層生成並指派一個全域唯一 ID。此 ID 不依賴資料庫,贯穿物件生命週期,作為 equals()/hashCode() 的唯一依據。
public abstract class AbstractPersistentObject implements PersistentObject {
// 實例化即生成 UUID,無需等待持久化
private String id = IdGenerator.createId();
private Integer version;
// Getter/Setter 省略...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof PersistentObject)) return false;
PersistentObject other = (PersistentObject) o;
if (id == null) return false;
return id.equals(other.getId());
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : super.hashCode();
}
}
關鍵在於告知 Hibernate:ID 由應用指派,判斷「新物件」改用 version 欄位:
<class name="Person" table="PERSON">
<id name="id" column="ID">
<generator class="assigned"/> <!-- 關鍵:ID 由應用指派 -->
</id>
<version name="version" column="VERSION" unsaved-value="null"/> <!-- 用 version 判斷新舊 -->
</class>
UUID(Universally Unique Identifier)為 128 位標準識別碼,跨 JVM、跨機器唯一,無需資料庫協調,極適合分散式環境。Java 5+ 內建 java.util.UUID.randomUUID(),舊版本可用 Apache Commons ID 或 JUG 庫。
public class IdGenerator {
public static String createId() {
return java.util.UUID.randomUUID().toString(); // 標準 36 字符格式
}
}
若資料庫不支援 UUID 作為主鍵(如效能考量),可採用 long 型 ID,由統一服務從資料庫序列獲取,於物件建構時注入,同樣達成「實例化即有 ID」目標。
PersistentObject 介面保持靈活。將標識符生成權收回應用層,是領域驅動設計中「實體身份識別」原則的自然體現。這不僅解決技術難題,更讓領域模型回歸純淨、專注於業務邏輯。