正確映射實體關聯,不僅影響 SQL 生成效率,更決定了領域模型的表達力。本文系統梳理三大關聯類型,對比外鍵關聯 vs 關聯表兩種映射策略,深度解析單向 vs 雙向、集合類型選型、級聯與抓取策略,附最佳實踐避雷指南。
表結構:多的一方(Address)持有外鍵 customer_id
CREATE TABLE customer (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100)
);
CREATE TABLE address (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
province VARCHAR(50),
city VARCHAR(50),
customer_id BIGINT, -- 外鍵在多的一方
FOREIGN KEY (customer_id) REFERENCES customer(id)
);
實體映射:
@Entity
@Table(name = "customer")
public class Customer {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "customer_id") // 指定外鍵列
private List<Address> addresses = new ArrayList<>();
// getter/setter
}
@Entity
@Table(name = "address")
public class Address {
@Id @GeneratedValue
private Long id;
private String province;
private String city;
// 無需映射 customer 字段(單向)
}
關鍵註解:
@JoinColumn(name="customer_id"):指定外鍵列名,必須在一的一方聲明mappedBy 表示單向,由 Customer 維護關聯@JoinColumn(insertable=false, updatable=false) 優化)表結構:中間表 customer_address
CREATE TABLE customer_address (
customer_id BIGINT NOT NULL,
address_id BIGINT NOT NULL UNIQUE,
FOREIGN KEY (customer_id) REFERENCES customer(id),
FOREIGN KEY (address_id) REFERENCES address(id)
);
實體映射:
@Entity
public class Customer {
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(
name = "customer_address",
joinColumns = @JoinColumn(name = "customer_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "address_id", referencedColumnName = "id")
)
private List<Address> addresses = new ArrayList<>();
}
優勢:Address 表無外鍵列,領域模型更純淨;易演進為 @ManyToMany。
多的一方(Address)擁有關聯所有權:
@Entity
public class Address {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}
一的一方(Customer)用 mappedBy 放棄維護權:
@Entity
public class Customer {
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Address> addresses = new ArrayList<>();
// 便利方法:雙向同步
public void addAddress(Address address) {
addresses.add(address);
address.setCustomer(this);
}
public void removeAddress(Address address) {
addresses.remove(address);
address.setCustomer(null);
}
}
核心規則:
mappedBy 值 = 多的一方實體屬性名(customer)通常作為雙向一對多的「多的一方」出現,單向用法同上。默認抓取策略為 EAGER,建議顯式設置 fetch = FetchType.LAZY 避免 N+1 問題。
本質是兩個 @OneToMany 的組合,必須使用關聯表。
@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
@Entity
public class Course {
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
注意:多對多雙向同樣需 mappedBy,維護權任選一方,建議在業務主導方維護。
| 集合類型 | 適用場景 | 初始化建議 | 性能特點 |
|---|---|---|---|
List | 有序、允許重複、需按索引訪問 | new ArrayList<>() | 有序,支援 @OrderColumn 維護順序,但刪除中間元素需更新後續索引 |
Set | 無序、不允許重複、推薦默認選擇 | new HashSet<>() | 去重語義天然匹配關聯,刪除高效,需正確實現 equals/hashCode |
Bag (Hibernate) | 無序、允許重複、不需索引 | new ArrayList<>() (映射為 Bag) | 不維護順序,刪除不需更新索引,但無去重 |
Map | Key-Value 關聯,如 Map<String, Address> | new HashMap<>() | 需 @MapKey 或 @MapKeyColumn 指定 Key 來源 |
CascadeType.REMOVE 導致關聯實體被誤刪@BatchSize(size=20) 或 hibernate.default_batch_fetch_size 可大幅減少 N+1JPA 關聯映射的核心決策矩陣:
| 決策點 | 推薦選擇 |
|---|---|
| 一對多 vs 多對一 | 業務上「一」聚合「多」用 @OneToMany + @ManyToOne 雙向 |
| 外鍵 vs 關聯表 | 簡單場景外鍵;需解耦/演進多對多用關聯表 |
| 集合類型 | 優先 Set<>(需正確 equals/hashCode),有序用 List+@OrderColumn |
| 抓取策略 | 全局 LAZY,查詢時用 Entity Graph / JOIN FETCH 精確控制 |
| 級聯策略 | 聚合根用 ALL/PERSIST/MERGE;非聚合根慎用 REMOVE |
掌握這些核心模式與避雷指南,能讓 JPA 關聯映射既符合領域語義,又生成高效 SQL,避免生產環境性能翻車。