Hibernate 映射的核心難點在於關聯關系建模與主鍵策略選擇。本文系統講解一對一關聯的四種變體、聯合主鍵映射、組件映射,並引入領域驅動設計(DDD)理念,展示從對象模型驅動數據庫架構的現代開發範式。
傳統開發:先建表 → 反向生成實體 → 開發業務
DDD 開發:先設計領域模型(實體、值對象、聚合根) → 映射配置 → SchemaExport 自動生成 DDL 腳本 → 執行建表
// 一鍵生成數據庫腳本並導入 AnnotationConfiguration config = new AnnotationConfiguration().configure(); SchemaExport export = new SchemaExport(config); // true: 輸出 SQL 到控制台, true: 直接執行建表 export.create(true, true);
優勢:模型變更只需修改實體/映射,重跑 SchemaExport 即可同步數據庫,消除手工維護 DDL 的錯誤與不同步風險。
場景:學生 — 身份證,學生表持有身份證外鍵 pid,身份證表無外鍵。
// 身份證:被引用方,無外鍵
@Entity
public class IdCard {
@Id
@GeneratedValue(generator = "pid")
@GenericGenerator(name = "pid", strategy = "assigned")
private String pid;
private String province;
// Getter/Setter
}
// 學生:主控方,持有外鍵
@Entity
public class Student {
@Id @GeneratedValue
private Integer sid;
private String sname;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "pid", unique = true) // 外鍵列,唯一約束
private IdCard cardId;
// Getter/Setter
}
<!-- IdCard.hbm.xml -->
<class name="IdCard" table="idcard">
<id name="pid" column="pid"><generator class="assigned"/></id>
<property name="province"/>
</class>
<!-- Student.hbm.xml -->
<class name="Student" table="student">
<id name="sid"><generator class="native"/></id>
<property name="sname"/>
<many-to-one name="cardId" column="pid" unique="true"/> <!-- 關鍵:many-to-one + unique=true -->
</class>
session.save(idCard); // 先保存被引用方(無外鍵依賴) session.save(student); // 再保存主控方(持有外鍵)
場景:雙方互持引用,但必須指定一方為關係擁有者(mappedBy),避免雙向維護導致死循環。
// 學生:擁有者,維護關係
@Entity
public class Student {
@Id @GeneratedValue
private Integer sid;
private String sname;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "pid", unique = true)
private IdCard cardId; // 外鍵在學生表
// Getter/Setter
}
// 身份證:被擁有方,mappedBy 指向擁有者的屬性名
@Entity
public class IdCard {
@Id @GenericGenerator(...)
private String pid;
private String province;
@OneToOne(mappedBy = "cardId") // 放棄維護權
private Student stu;
// Getter/Setter
}
// 身份證:擁有者,外鍵移至身份證表
@Entity
public class IdCard {
@Id ...
private String pid;
private String province;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "sid", unique = true)
private Student stu; // 外鍵在身份證表
}
// 學生:被擁有方
@Entity
public class Student {
@Id @GeneratedValue
private Integer sid;
private String sname;
@OneToOne(mappedBy = "stu")
private IdCard cardId;
}
生成表結構差異:擁有者表包含外鍵列,被擁有者表無外鍵。保存順序隨擁有者變化:先保存無外鍵的一方。
<!-- Student.hbm.xml (擁有者) --> <many-to-one name="cardId" column="pid" unique="true"/> <!-- IdCard.hbm.xml (被擁有者) --> <one-to-one name="stu" property-ref="cardId"/> <!-- property-ref 指向擁有者屬性 -->
定義:多列組合為主鍵,如訂單號 + 序列號唯一標識補充訂單。
// 聯合主鍵類:必須實現 Serializable、重寫 equals/hashCode
@Embeddable
public class IdCardPK implements Serializable {
private String pid;
private String bloodType; // 新增血型作為聯合主鍵一部分
// Getter/Setter, equals, hashCode
}
// 身份證:使用 @EmbeddedId
@Entity
public class IdCard {
@EmbeddedId
private IdCardPK pk;
private String province;
@OneToOne(mappedBy = "cardId")
private Student stu;
}
// 學生:@JoinColumns 映射多列外鍵
@Entity
public class Student {
@Id @GeneratedValue
private Integer sid;
private String sname;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumns({
@JoinColumn(name = "pid", referencedColumnName = "pid"),
@JoinColumn(name = "bloodType", referencedColumnName = "bloodType")
})
private IdCard cardId;
}
<!-- IdCard.hbm.xml -->
<class name="IdCard" table="idcard">
<composite-id name="pk" class="IdCardPK">
<key-property name="pid" column="pid"/>
<key-property name="bloodType" column="blood_type"/>
<generator class="assigned"/>
</composite-id>
<property name="province"/>
</class>
<!-- Student.hbm.xml -->
<many-to-one name="cardId">
<column name="pid" unique="true"/>
<column name="blood_id"/> <!-- 對應 bloodType -->
</many-to-one>
本質:單表,組件類無 OID(無主鍵、無獨立表),作為宿主實體的值對象嵌入。
// 組件類:純 POJO,無任何註解
public class IdCard {
private String pid;
private String province;
// Getter/Setter
}
// 宿主實體:@Embedded 嵌入組件
@Entity
public class Student {
@Id @GeneratedValue
private Integer sid;
private String sname;
@Embedded
private IdCard cardId; // 字段展平到 student 表
// Getter/Setter
}
生成表結構:student(sid, sname, pid, province),無 idcard 表。
<class name="Student" table="student">
<id name="sid"><generator class="native"/></id>
<property name="sname"/>
<component name="cardId" class="IdCard">
<property name="pid" column="pid"/>
<property name="province"/>
</component>
</class>
| 維度 | 實體關聯 | 組件/值對象 |
|---|---|---|
| 標識 | 有 OID (ID) | 無 OID,由宿主標識 |
| 生命周期 | 獨立,可被多方引用 | 依附宿主,隨宿主創建/銷毀 |
| 表結構 | 獨立表 + 外鍵 | 字段展平到宿主表 |
| DDD 對應 | Entity (實體) | Value Object (值對象) |
| 復用性 | 引用共享 | 嵌入複製,不同宿主可嵌入同類型組件 |
DDD 實戰建模示例:
// 細粒度領域模型
public class User {
private UserId id; // 實體標識
private Name name; // 值對象:firstName + lastName
private Contact contact; // 值對象:email + phone + address
private List<Address> addresses; // 實體集合(地址有獨立生命周期)
}
// 對應粗粒度數據庫表:user(id, first_name, last_name, email, phone, address)
// Address 獨立表:address(id, user_id, street, city, zip)
hibernate-jpa-2.0-api-1.0.1.Final.jarejb3-persistence.jar 與 hibernate-jpa-2.0-api.jar 衝突,刪除前者<mapping class="..."/> 或 <mapping resource="..."/> 必須齊全是否需要雙向導航?
├─ 否 → 單向外鍵(many-to-one + unique=true)✅ 最簡單
└─ 是 → 是否共享主鍵?
├─ 是 → 聯合主鍵 / 主鍵共享 (@MapsId / @PrimaryKeyJoinColumn)
└─ 否 → 雙向外鍵(指定 mappedBy 擁有者)
├─ 外鍵在哪張表?由業務語義決定(誰持有誰的生命周期)
└─ 保存順序:先保存無外鍵的一方
是否為值對象(無獨立標識、生命周期綁定宿主)?
├─ 是 → 組件映射 (@Embedded / ) ✅ 單表、高性能
└─ 否 → 實體關聯(上述各類一對一)
掌握這套映射體系,不僅能應對複雜的關聯建模挑戰,更能踐行 DDD「領域模型驅動數據庫設計」的核心思想,讓持久層真正成為領域模型的忠實投影,而非業務邏輯的妥協產物。