多對多關聯在領域模型中極為常見(如「學生-課程」、「用戶-角色」、「教師-學生」)。Hibernate 透過中間表 (Join Table) 實現,本文以「教師-學生」為例,完整示範單向與雙向映射的註解配置、生成 DDL 與最佳實踐。
| 實體 | 表名 | 主鍵 | 關鍵欄位 |
|---|---|---|---|
| Teacher | teacher | id | name |
| Student | student | id | name |
| 中間表 | t_s (自定義) | teacher_id, student_id (聯合主鍵) | 兩外鍵分別指向 teacher.id、student.id |
@Entity
public class Teacher {
@Id @GeneratedValue
private Integer id;
private String name;
@ManyToMany
@JoinTable(
name = "t_s",
joinColumns = @JoinColumn(name = "teacher_id"),
inverseJoinColumns = @JoinColumn(name = "student_id")
)
private Set<Student> students = new HashSet<>();
// getters/setters 略
}
@Entity
public class Student {
@Id @GeneratedValue
private Integer id;
private String name;
// 無反向導航屬性
// getters/setters 略
}
create table teacher (
id integer not null auto_increment,
name varchar(255),
primary key (id)
);
create table student (
id integer not null auto_increment,
name varchar(255),
primary key (id)
);
create table t_s (
teacher_id integer not null,
student_id integer not null,
primary key (teacher_id, student_id)
);
alter table t_s add constraint FK_teacher foreign key (teacher_id) references teacher (id);
alter table t_s add constraint FK_student foreign key (student_id) references student (id);
mappedBy 指定擁有方)@Entity
public class Teacher {
@Id @GeneratedValue
private Integer id;
private String name;
@ManyToMany
@JoinTable(
name = "t_s",
joinColumns = @JoinColumn(name = "teacher_id"),
inverseJoinColumns = @JoinColumn(name = "student_id")
)
private Set<Student> students = new HashSet<>();
// getters/setters
}
@Entity
public class Student {
@Id @GeneratedValue
private Integer id;
private String name;
@ManyToMany(mappedBy = "students") // 關鍵:放棄關聯維護權,指向擁有方屬性名
private Set<Teacher> teachers = new HashSet<>();
// getters/setters
}
中間表結構無差異,mappedBy 僅影響 Hibernate 運行時行為,不影響 Schema。
// 在 Teacher 端提供便利方法,確保雙向一致性
public void addStudent(Student student) {
this.students.add(student);
student.getTeachers().add(this); // 同步反向
}
public void removeStudent(Student student) {
this.students.remove(student);
student.getTeachers().remove(this);
}
鐵律:只在擁有方 (Teacher) 修改集合,並在便利方法中同步反向;反向方 (Student) 的 setter 僅供 Hibernate 內部使用,業務代碼嚴禁直接調用。
| 需求 | 配置 | 說明 |
|---|---|---|
| 自定義中間表欄位名 | @JoinColumn(name="t_id") / @JoinColumn(name="s_id") | 適配既有資料庫命名規範 |
| 中間表額外欄位 (如建立時間) | 改用 @OneToMany + 中間實體類 | 純多對多無法映射中間表額外欄位 |
| 延遲載入 (預設) | @ManyToMany(fetch = FetchType.LAZY) | 避免載入大量關聯對象,配合 DTO 投影查詢 |
| 急切載入 | @EntityGraph 或 JPQL JOIN FETCH | 特定業務場景一次性載入,防止 N+1 |
| 級聯操作 | cascade = {CascadeType.PERSIST, CascadeType.MERGE} | 謹慎使用 REMOVE,會刪除被關聯實體! |
| 唯一約束 | uniqueConstraints = @UniqueConstraint(columnNames = {"teacher_id","student_id"}) | DDL 已含聯合主鍵,通常無需額外聲明 |
setStudents() 不同步反向,持久化時 Hibernate 以擁有方為準,反向關聯不生效。cascade = CascadeType.ALL 或 REMOVE 會刪除 Student,通常業務不允許。建議僅 PERSIST, MERGE。@JsonIgnore (Jackson) 或 @ToString.Exclude (Lombok) 標記反向屬性。teacher.getStudents() 觸發全量載入。解決:@BatchSize(size=100)、分頁查詢 DTO、或拆分為單向關聯 + 專用查詢方法。@Index(name="idx_student", columnList="student_id") (需配合 @Table(indexes=...) 在中間實體類上,純註解映射無法直接加索引,建議手動 DDL)。<set name="students" table="t_s" lazy="true">
<key column="teacher_id"/>
<many-to-many class="Student" column="student_id"/>
</set>
-- 雙向在 Student 端
<set name="teachers" table="t_s" inverse="true" lazy="true">
<key column="student_id"/>
<many-to-many class="Teacher" column="teacher_id"/>
</set>
inverse="true" 對應註解的 mappedBy,表示放棄關聯維護權。
是否需要從 Student 端導航到 Teachers?
├─ 否 → 單向 @ManyToMany (Teacher 擁有方) ✅ 推薦
└─ 是 → 雙向 @ManyToMany + mappedBy
├─ 集合較小、頻雙向遍歷 → 可接受
├─ 集合大、高併發 → 拆分為兩個單向關聯 + 專用 Repository 查詢方法
└─ 中間表需額外欄位 → 改用 @OneToMany + 關聯實體類