首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Hibernate 一對多單向與雙向關聯映射深度解析:映射配置、SQL 生成、數據操作與性能優化

     編輯:Java 相關知識

前言

一對多關聯是領域模型中最常見的關係(如:班級-學生、訂單-訂單項)。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>

生成 DDL

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>

inverse="true" 核心含義

  • 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))。

三、讀取操作與 N+1 問題

// 加載班級
Classes c = session.get(Classes.class, 1);
// 遍歷學生 (觸發懶加載)
for (Student s : c.getStudents()) { System.out.println(s.getName()); }
  • 默認 <set> 為 懶加載 (lazy="true"),首次遍歷時發 SQL 查詢學生。
  • N+1 問題: 若批量加載多個班級並遍歷學生,會發 1 條查班級 + N 條查學生。優化:HQL 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:從集合移除實體時自動刪除數據庫記錄。

五、性能優化清單

  1. 雙向 + inverse="true":消除冗餘 UPDATE。
  2. 批量抓取: @BatchSize 或 HQL left join fetch 解決 N+1。
  3. 合理級聯: cascade = {PERSIST, MERGE} 避免誤刪,orphanRemoval 處理孤兒。
  4. 集合類型: Set 避免重複,List 需 @OrderColumn 維護順序(額外開銷)。
  5. 避免大集合: 一個班級不應載入萬級學生,改用分頁查詢或單獨 Repository 方法。

總結

一對多映射的核心決策:單向簡單但有冗餘 SQL,雙向配合 inverse="true" 性能最優但需手動維護兩端一致性。現代項目推薦註解映射 + mappedBy + @BatchSize,並封裝關聯管理工具方法,將關聯維護邏輯內聚到領域模型內部。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved