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

Hibernate 多對一關聯映射實戰 (N-1):註解配置、雙向關聯、復合主鍵外鍵、級聯操作、雙向一致性維護

     編輯:Java 綜合教學

Hibernate 多對一關聯映射實戰 (N-1):註解配置、雙向關聯、復合主鍵外鍵、級聯操作、雙向一致性維護

多對一 (@ManyToOne) 是關聯映射最基礎也是最常用的模式。本文以「人員-地址」為例,系統講解單向/雙向映射、復合主鍵作為外鍵的 @JoinColumns 配置、mappedBy 雙向一致性維護、級聯類型選擇、FetchType 效能調優、資料庫腳本與完整測試代碼。

一、領域模型設計

  • People (1 方):復合主鍵 (first + last),一個人可有多個地址。
  • Address (N 方):單一主鍵 addressId (自增),屬於一個 People。
  • 關聯:People 1 → N Address (雙向),Address 為關聯維護端 (擁有外鍵)。

二、資料庫腳本 (MySQL)

CREATE DATABASE hibernate CHARACTER SET utf8mb4;
USE hibernate;

CREATE TABLE people_inf (
    first VARCHAR(50) NOT NULL,
    last VARCHAR(50) NOT NULL,
    age INT,
    PRIMARY KEY (first, last)
) ENGINE=InnoDB;

CREATE TABLE address_inf (
    address_id INT AUTO_INCREMENT PRIMARY KEY,
    message VARCHAR(255),
    people_first VARCHAR(50) NOT NULL,
    people_last VARCHAR(50) NOT NULL,
    FOREIGN KEY (people_first, people_last) REFERENCES people_inf(first, last)
) ENGINE=InnoDB;

三、實體類註解映射

1. People (一端,被引用端)

@Entity
@Table(name = "people_inf")
public class People implements Serializable {
    @Id
    @Column(name = "first", length = 50)
    private String first;

    @Id
    @Column(name = "last", length = 50)
    private String last;

    private int age;

    // 雙向一對多:mappedBy 指向 Address 中的屬性名 "people"
    @OneToMany(mappedBy = "people", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Address> addresses = new HashSet<>();

    // Getter/Setter/equals/hashCode (基於復合主鍵) 省略

    // 便利方法:維護雙向一致性
    public void addAddress(Address address) {
        addresses.add(address);
        address.setPeople(this); // 關鍵:設置反向引用
    }
    public void removeAddress(Address address) {
        addresses.remove(address);
        address.setPeople(null);
    }
}

2. Address (多端,關聯維護端)

@Entity
@Table(name = "address_inf")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "address_id")
    private Integer addressId;

    private String message;

    // 多對一:@JoinColumns 對應復合外鍵
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
        @JoinColumn(name = "people_first", referencedColumnName = "first", nullable = false),
        @JoinColumn(name = "people_last",  referencedColumnName = "last",  nullable = false)
    })
    private People people;

    // Getter/Setter 省略
}

四、關鍵註解深度解析

註解位置核心作用
@ManyToOneAddress.people定義多對一關聯,預設 FetchType.LAZY (延遲載入)
@JoinColumnsAddress.people映射復合外鍵,每個 @JoinColumn 對應一組 (外鍵列, 參考主鍵列)
@OneToManyPeople.addresses定義一對多反向關聯
mappedBy="people"People.addresses**關鍵**:聲明關聯由 Address.people 維護,People 端不生成外鍵、不維護關聯表
cascade=CascadeType.ALLPeople.addresses持久化/合併/刪除/刷新/脫管 級聯到地址集合
orphanRemoval=truePeople.addresses (可選)從集合移除 Address 時自動刪除該 Address (防止孤兒)

五、Hibernate 配置與測試代碼

hibernate.cfg.xml (精簡版)

<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost:3306/hibernate?useSSL=false&serverTimezone=UTC</property>
<property name="connection.username">root</property>
<property name="connection.password">123456</property>
<property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
<property name="show_sql">true</property>
<property name="hbm2ddl.auto">update</property>
<mapping class="com.example.People"/>
<mapping class="com.example.Address"/>

測試案例

public class ManyToOneTest {
    private static SessionFactory sf;

    @BeforeAll
    static void init() {
        sf = new Configuration().configure().buildSessionFactory();
    }

    @Test
    void testSaveCascade() {
        Session s = sf.openSession(); Transaction tx = s.beginTransaction();

        People p = new People();
        p.setFirst("林"); p.setLast("彪"); p.setAge(22);

        Address a1 = new Address(); a1.setMessage("廣州");
        Address a2 = new Address(); a2.setMessage("茂名");

        p.addAddress(a1); // 維護雙向引用
        p.addAddress(a2);

        s.save(p); // 級聯保存 a1, a2 (CascadeType.ALL)
        tx.commit(); s.close();
    }

    @Test
    void testBidirectionalConsistency() {
        Session s = sf.openSession(); Transaction tx = s.beginTransaction();

        People p = s.get(People.class, new PeopleId("林", "彪")); // 復合主鍵查詢
        // 懶加載觸發
        System.out.println("地址數: " + p.getAddresses().size()); // SELECT * FROM address WHERE people_first='林' AND people_last='彪'

        // 新增地址 (雙向一致性)
        Address a3 = new Address(); a3.setMessage("深圳");
        p.addAddress(a3); // 設置反向引用
        s.save(a3); // 顯式保存新地址 (或依賴 cascade)

        tx.commit(); s.close();
    }

    @Test
    void testOrphanRemoval() {
        // 需在 People.addresses 加 @OneToMany(orphanRemoval=true)
        Session s = sf.openSession(); Transaction tx = s.beginTransaction();
        People p = s.get(People.class, new PeopleId("林", "彪"));
        Address toRemove = p.getAddresses().iterator().next();
        p.removeAddress(toRemove); // 從集合移除
        // 提交時自動 DELETE 被移除的 Address (orphanRemoval)
        tx.commit(); s.close();
    }

    @AfterAll
    static void destroy() { sf.close(); }
}

六、效能調優建議

  1. FetchType.LAZY (預設):避免載入 People 時連帶查詢所有 Address。需用時再查。
  2. @BatchSize(size=10) 在 People.addresses 上:集合延遲載入時批量抓取,減少 N+1 查詢。
  3. @Fetch(FetchMode.SUBSELECT):子查詢一次載入所有集合,適合已知會遍歷集合的場景。
  4. 避免在 toString()/equals()/hashCode() 中觸發關聯:導致意外載入,建議僅基於主鍵實現 equals/hashCode。

七、常見錯誤與排查

現象原因修正
外鍵欄位為 NULL未設置 Address.people 或 People.addAddress 未調用保存前確保雙向引用一致:p.addAddress(a) 或 a.setPeople(p)
Duplicate entry 錯誤復合主鍵 PeopleId 未正確實現 equals/hashCode基於 first+last 實現,或用 @EmbeddedId 封裝
懶加載失敗 (no Session)Session 關閉後訪問 LAZY 集合Open Session In View Filter / DTO 組裝 / fetch join / 初始化後再關閉
級聯刪除誤刪CascadeType.REMOVE + 雙向關聯導致連鎖刪除謹慎使用 REMOVE/ALL,考慮 orphanRemoval 替代

八、進階:@EmbeddedId 封裝復合主鍵 (推薦)

@Embeddable
public class PeopleId implements Serializable {
    private String first;
    private String last;
    // equals/hashCode 基於 first+last
}

@Entity
public class People {
    @EmbeddedId
    private PeopleId id; // 替代兩個 @Id
    // ...
}

// 查詢
People p = session.get(People.class, new PeopleId("林", "彪"));
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved