一對多關聯是領域模型中最常見的關係(如:班級-學生、訂單-訂單項)。Hibernate 提供單向與雙向兩種映射方式,本文通過「班級-學生」經典案例,深度解析映射配置差異、生成 SQL、數據操作細節、級聯行為與性能優化建議。
// 班級 (一的一方)
public class Classes {
private Integer id;
private String name;
private Set<Student> students = new HashSet<>(); // 單向/雙向均需
// Getter/Setter
}
// 學生 (多的一方)
public class Student {
private Integer id;
private String name;
private Classes classes; // 僅雙向需要
// Getter/Setter
}
<!-- Classes.hbm.xml -->
<class name="Classes" table="t_classes">
<id name="id"><generator class="native"/></id>
<property name="name"/>
<set name="students">
<key column="classes_id"/> -- 外鍵列
<one-to-many class="Student"/> -- 目標實體
</set>
</class>
<!-- Student.hbm.xml -->
<class name="Student" table="t_student">
<id name="id"><generator class="native"/></id>
<property name="name"/>
</class>
create table t_classes (id integer not null auto_increment, name varchar(255), primary key (id));
create table t_student (id integer not null auto_increment, name varchar(255), classes_id integer, primary key (id));
alter table t_student add constraint FK_student_classes foreign key (classes_id) references t_classes (id);
classes_id 位於「多的一方」表 t_student 中。Classes 通過 <set> 維護。// 保存:先存學生,再存班級並關聯
Session s = sessionFactory.openSession(); Transaction tx = s.beginTransaction();
Student s1 = new Student("張三"); Student s2 = new Student("李四");
s.save(s1); s.save(s2);
Classes c = new Classes("一班");
c.getStudents().add(s1); c.getStudents().add(s2);
s.save(c); // 會發額外 UPDATE 維護外鍵
tx.commit();
缺點: 保存時會額外發送 UPDATE t_student SET classes_id = ? WHERE id = ? 維護外鍵,因為學生端不知曉關聯。
<!-- Student.hbm.xml 新增 -->
<many-to-one name="classes" column="classes_id" />
<!-- Classes.hbm.xml 關鍵屬性 inverse="true" -->
<set name="students" inverse="true">
<key column="classes_id"/>
<one-to-many class="Student"/>
</set>
false (默認):「一的一方」負責維護關係 → 保存雙方都會發 UPDATE 維護外鍵 → 冗餘 SQL。true:「多的一方」負責維護關係 → 僅在設置 student.setClasses(classes) 時發 UPDATE → 消除冗餘。// 方式 1:先存班級,再存學生 (推薦用於雙向)
Classes c = new Classes("一班"); s.save(c);
Student s1 = new Student("張三"); s1.setClasses(c); s.save(s1); // 此處發 UPDATE 維護外鍵
Student s2 = new Student("李四"); s2.setClasses(c); s.save(s2);
// 方式 2:先存學生,再存班級
Student s1 = new Student("張三"); s.save(s1); // classes_id = NULL
Classes c = new Classes("一班");
c.getStudents().add(s1); // 雙向需手動維護另一端:s1.setClasses(c);
s.save(c); // 此時 UPDATE 將 NULL → 實際 ID
關鍵: 雙向關聯在內存中必須手動維護兩端一致性(工具方法 addStudent(Student s) 同時設置 s.setClasses(this) 和 students.add(s))。
// 加載班級
Classes c = session.get(Classes.class, 1);
// 遍歷學生 (觸發懶加載)
for (Student s : c.getStudents()) { System.out.println(s.getName()); }
<set> 為 懶加載 (lazy="true"),首次遍歷時發 SQL 查詢學生。left join fetch、@BatchSize、二級緩存。// Classes
@OneToMany(mappedBy = "classes", cascade = CascadeType.ALL, orphanRemoval = true)
@BatchSize(size = 20) // 優化 N+1
private Set<Student> students = new HashSet<>();
// Student
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "classes_id")
private Classes classes;
mappedBy 指向關聯擁有端字段名,對應 inverse="true"。orphanRemoval = true:從集合移除實體時自動刪除數據庫記錄。@BatchSize 或 HQL left join fetch 解決 N+1。cascade = {PERSIST, MERGE} 避免誤刪,orphanRemoval 處理孤兒。Set 避免重複,List 需 @OrderColumn 維護順序(額外開銷)。一對多映射的核心決策:單向簡單但有冗餘 SQL,雙向配合 inverse="true" 性能最優但需手動維護兩端一致性。現代項目推薦註解映射 + mappedBy + @BatchSize,並封裝關聯管理工具方法,將關聯維護邏輯內聚到領域模型內部。