關聯映射是 Hibernate ORM 核心能力,將資料庫外鍵關係映射為物件導航關係。本文系統整理單向/雙向多對一、一對多、多對多四大關聯類型的 XML (HBM) 與 JPA 註解配置,含關鍵屬性解析、級聯策略、inverse 反向控制與最佳實踐。
-- 部門表
CREATE TABLE dept (
dept_no INT PRIMARY KEY AUTO_INCREMENT,
dname VARCHAR(50), loc VARCHAR(50)
);
-- 員工表
CREATE TABLE emp (
emp_no INT PRIMARY KEY AUTO_INCREMENT,
ename VARCHAR(50),
dept_no INT,
FOREIGN KEY (dept_no) REFERENCES dept(dept_no)
);
-- 項目表
CREATE TABLE project (
pro_no INT PRIMARY KEY AUTO_INCREMENT,
pname VARCHAR(50)
);
-- 員工-項目關聯表
CREATE TABLE proemp (
rpro_no INT, remp_no INT,
PRIMARY KEY (rpro_no, remp_no),
FOREIGN KEY (rpro_no) REFERENCES project(pro_no),
FOREIGN KEY (remp_no) REFERENCES emp(emp_no)
);
場景:員工屬於一部門,Employee 導航 Dept,Dept 不導航 Employee。
<class name="Employee" table="emp">
<id name="empno" column="emp_no"><generator class="native"/></id>
<property name="ename" column="ename"/>
<!-- 多對一:name=屬性名,column=外鍵列,class=目標實體 -->
<many-to-one name="dept" column="dept_no" class="Dept"/>
</class>
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_no") // 外鍵列名
private Dept dept;
場景:部門擁有多員工,員工屬於一部門,雙向導航。
<!-- Dept.hbm.xml (一端) -->
<class name="Dept" table="dept">
<id name="deptNo" column="dept_no"><generator class="native"/></id>
<property name="dname" column="dname"/>
<!-- 一對多:set 集合,key=外鍵列,one-to-many=目標實體 -->
<set name="emps" cascade="save-update" inverse="true">
<key column="dept_no"/>
<one-to-many class="Employee"/>
</set>
</class>
<!-- Employee.hbm.xml (多端) -->
<class name="Employee" table="emp">
<id name="empno" column="emp_no"><generator class="native"/></id>
<property name="ename" column="ename"/>
<many-to-one name="dept" column="dept_no" class="Dept"/>
</class>
// Dept.java (一端 - 反向端)
@OneToMany(mappedBy = "dept", cascade = CascadeType.ALL)
private Set<Employee> emps = new HashSet<>();
// Employee.java (多端 - 維護端)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_no")
private Dept dept;
dept_nosave-update (持久化/更新傳播)、delete (刪除傳播)、all (全傳播)dept.getEmps().add(emp); emp.setDept(dept); 保證對象圖一致場景:員工參與多項目,僅 Employee 導航 Project,Project 不導航 Employee。
<class name="Employee" table="emp">
<id name="empno" column="emp_no"><generator class="native"/></id>
<property name="ename" column="ename"/>
<many-to-one name="dept" column="dept_no" class="Dept"/>
<!-- 多對多:set 集合,table=關聯表,key=本端外鍵,many-to-many=對端外鍵+類 -->
<set name="projects" table="proemp" inverse="true">
<key column="remp_no"/>
<many-to-many class="Project" column="rpro_no"/>
</set>
</class>
@ManyToMany
@JoinTable(
name = "proemp",
joinColumns = @JoinColumn(name = "remp_no"), // 本端外鍵
inverseJoinColumns = @JoinColumn(name = "rpro_no") // 對端外鍵
)
private Set<Project> projects = new HashSet<>();
場景:員工參與多項目,項目包含多員工,雙向導航。
<!-- Employee.hbm.xml (維護端 inverse=false 默認) -->
<set name="projects" table="proemp" cascade="save-update">
<key column="remp_no"/>
<many-to-many class="Project" column="rpro_no"/>
</set>
<!-- Project.hbm.xml (反向端 inverse=true) -->
<set name="employees" table="proemp" cascade="save-update" inverse="true">
<key column="rpro_no"/>
<many-to-many class="Employee" column="remp_no"/>
</set>
// Employee.java (維護端)
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "proemp",
joinColumns = @JoinColumn(name = "remp_no"),
inverseJoinColumns = @JoinColumn(name = "rpro_no")
)
private Set<Project> projects = new HashSet<>();
// Project.java (反向端)
@ManyToMany(mappedBy = "projects")
private Set<Employee> employees = new HashSet<>();
| 概念 | XML (HBM) | JPA 註解 | 說明 |
|---|---|---|---|
| 多對一 | <many-to-one name="dept" column="dept_no" class="Dept"/> | @ManyToOne @JoinColumn(name="dept_no") | 多端持有單個一端引用 |
| 一對多 | <set name="emps"><key column="dept_no"/><one-to-many class="Employee"/></set> | @OneToMany(mappedBy="dept") | 一端持有多端集合 |
| 多對多 | <set name="projects" table="proemp"><key column="remp_no"/><many-to-many class="Project" column="rpro_no"/></set> | @ManyToMany @JoinTable(...) | 雙端持有對方集合,需中間表 |
| 維護端 | inverse="false" (默認) | 無 mappedBy (或 @JoinTable 定義端) | 負責更新外鍵/中間表 |
| 反向端 | inverse="true" | mappedBy="屬性名" | 放棄維護,僅導航查詢 |
| 級聯持久化 | cascade="save-update" | cascade={PERSIST, MERGE} | 保存/更新傳播到關聯對象 |
| 級聯刪除 | cascade="delete" | cascade=REMOVE | 刪除傳播 (慎用!) |
| 級聯全傳播 | cascade="all" | cascade=ALL | 所有操作傳播 |
| 孤兒刪除 | cascade="delete-orphan" (僅 one-to-many) | orphanRemoval=true | 從集合移除即刪除實體 |
// 新增部門及員工 (級聯保存)
Dept dept = new Dept(); dept.setDname("技術部");
Employee emp1 = new Employee(); emp1.setEname("張三");
Employee emp2 = new Employee(); emp2.setEname("李四");
// 雙邊建立關聯
dept.getEmps().add(emp1); dept.getEmps().add(emp2);
emp1.setDept(dept); emp2.setDept(dept);
session.save(dept); // cascade="save-update" 自動保存 emp1, emp2
Employee emp = session.get(Employee.class, 1L);
Project p1 = session.get(Project.class, 1L);
Project p2 = session.get(Project.class, 2L);
// 雙邊添加關聯
emp.getProjects().add(p1); emp.getProjects().add(p2);
p1.getEmployees().add(emp); p2.getEmployees().add(emp);
session.saveOrUpdate(emp); // 維護端操作同步中間表
// 僅移除員工與項目關聯
emp.getProjects().remove(p1);
p1.getEmployees().remove(emp);
session.update(emp);
| 陷阱 | 現象 | 根因 | 解決 |
|---|---|---|---|
| 雙向一對多雙方 inverse=false | 外鍵更新兩次、效能差 | 雙方都維護關係 | 一端 mappedBy/inverse=true |
| 多對多雙方 inverse=false | 中間表主鍵衝突異常 | 雙方都 INSERT 中間表 | 一方 inverse=true |
| 多對多雙方 inverse=true | 中間表無數據 | 雙方都放棄維護 | 一方 inverse=false |
| 單邊設置關聯 | 關係不持久化/對象圖不一致 | 維護端未感知變化 | 雙邊同步設置 |
| cascade="delete" 刪除一端 | 誤刪關聯對象實體 | 刪除傳播到多端 | 用 save-update 或手動管理 |
| Set 未初始化 | NPE | 聲明未實例化 | new HashSet<>() |
| 實體無 equals/hashCode | Set 去重失效、異常 | 集合元素需正確實現 | 基於 ID 實現 |
@BatchSize(size=20) 或 hibernate.default_batch_fetch_size=20 解決 N+1@LazyCollection(LazyCollectionOption.EXTRA) 或 HQL 分頁查詢核心心法:關聯映射的靈魂是「誰維護外鍵/中間表」。XML 用
inverse,註解用mappedBy。維護端操作集合同步數據庫,反向端僅導航。雙邊內存同步保證對象圖一致。選擇集合較小端維護是效能鐵律。級聯策略謹慎選擇,delete/all極易誤刪數據。
掌握關聯映射配置,能讓 ORM 真正發揮「對象導航替代 SQL JOIN」的生產力優勢,同時避免 N+1、死循環、數據不一致等經典坑。