1. 程式人生 > >QQA: Hibernate 為什麼需要手工管理雙向關聯

QQA: Hibernate 為什麼需要手工管理雙向關聯

Hibernate/JPA 中如果兩個 Entity 之間的關聯是雙向的(不論是 @ManyToMany@OneToMany 還是 @OneToOne),都需要手動管理關聯,為什麼?

  • 呼叫 entityManager.persist 儲存物件時 Hibernate/JPA 不會直接執行 SQL,而會等到 entityManager.flush 或事務 commit 時完成。
  • 同理 entityManager.load 也可能只會從記憶體中獲取物件(可以認為是某種快取)。
  • 如果不手動管理雙向關聯,則從記憶體獲取的物件並不會反映資料庫中的對映關係。

#什麼是雙向關聯

雙向關聯的本質是告訴 Hibernate 讓兩個實體共用一張資料庫表(或表結構)。

這裡以 @ManyToMany 為例(參考Hibernate User Guide) :有兩個實體 PersonAddress,一個 Person 可以擁有多個 Address,而一個 Address 也可以屬於多個 Person。於是設計實體如下:

@Entitypublic static class Person {    @Id    @GeneratedValue    private Long id;    @ManyToMany    private List<Address> addresses = new ArrayList<>();    // ... omit all other stuff
}@Entitypublic static class Address { @Id @GeneratedValue private Long id; @ManyToMany private List<Person> owners = new ArrayList<>(); // ... omit all other stuff}

問題來了,我們應該建立一張關聯表還是兩張呢?其實取決於使用業務含義。即如果 Personaddresses 的含義是“人的居住地址”,而 Address 中的 owners 與之對應,表達的是“地址上居住的人”,則它們應該是一張關聯表。但如果 Address

owners 表達的是“地址的主人(如房東)”,則二者就不應該共用一張關聯表。

如何告訴 Hibernate 需要共用一張表呢?通過 mappedBy

@Entitypublic static class Person {    @ManyToMany    private List<Address> addresses = new ArrayList<>();    // ... omit all other methods}@Entitypublic static class Address {    @ManyToMany(mappedBy = "addresses")    private List<Person> owners = new ArrayList<>();    // ... omit all other methods}

(mappedBy = "addresses") 的含義是這個欄位與 Person 中的 addresses 欄位共用表結構。

這裡最後重點是雙向關係一定是從屬關係,有一方是 owner,另一方是 follower(標記了 mappedBy 的一方)。只有在 owner 這方新增關聯並儲存時,Hibernate 才會存入關聯表,反之不會。例如我們只能通過 person.addAddress() 並儲存 person 的方式來完成新增關聯而不能用 address.addPerson() 後儲存 address 的方式。

#手工管理關聯是什麼意思

例如我們在實現 Person.addAddress 時,需要這樣實現:

@Entitypublic static class Person {    //...omit other fields    @ManyToMany    private List<Address> addresses = new ArrayList<>();    public void addAddress(Address address) {        addresses.add( address );        address.getOwners().add( this );    }    public void removeAddress(Address address) {        addresses.remove( address );        address.getOwners().remove( this );    }    // ... omit all other methods}

即在為 person 新增 address 時,我們需要將當前的 person 新增到 address的 owners 欄位中;刪除時相似。“管理關聯”表示需要在程式碼級別來管理關聯雙方實體的聯絡。

如果從資料庫的角度思考,我們知道 PersonAddress 的關係是儲存在一張關聯表裡的,一個關聯存入這張表後,不論哪一方讀取,都應該反映出新的關聯關係,而在 Hibernate 這一層,卻需要我們顯式地(從另一方的 set )中新增/刪除這個關聯,顯得不可思議。

另外,注意我們往 set 中新增 addressperson 時,需要我們正確的實現 PersonAddressequalshashCode 方法,這是另一個坑,這裡就不深入了。

#為什麼需要手工管理

終於到了“為什麼”部分了,首先是如果不手工管理會發生什麼。考慮下面的測試:

@Test@Transactionalpublic void test() {    Person person = repository.findPersonById(1);    Address address = repository.findAddressById(20);    person.getAddresses.add(address);    repository.save(person);    System.out.println(address.getOwners().size()) // what is the result?    Address address = repository.findAddressById(20);    System.out.println(address.getOwners().size()) // what is the result?}

答案是兩個 size 都為 0

  • 呼叫 save 方法時,Hibernate/JPA 並不會直接執行 SQL 來儲存,這樣效能差。
  • find 時,如果記憶體中已經有對應的物件,Hibernate/JPA 也不會執行 SQL 去查詢。

注意上面說的是一般的情況,什麼時候執行 SQL 取決於具體的配置,一般會在事務前的 commit

因此,如果在 save 之後還需要使用到 address,就不要期待它會立即反映出資料庫中的修改;反之,如果 save 之後就不再使用到 address,那即使不手工管理(同步) 關聯關係也不會有多大影響。