QQA: Hibernate 為什麼需要手工管理雙向關聯
Hibernate/JPA 中如果兩個 Entity 之間的關聯是雙向的(不論是 @ManyToMany
、
@OneToMany
還是 @OneToOne
),都需要手動管理關聯,為什麼?
- 呼叫
entityManager.persist
儲存物件時 Hibernate/JPA 不會直接執行 SQL,而會等到entityManager.flush
或事務commit
時完成。 - 同理
entityManager.load
也可能只會從記憶體中獲取物件(可以認為是某種快取)。 - 如果不手動管理雙向關聯,則從記憶體獲取的物件並不會反映資料庫中的對映關係。
#什麼是雙向關聯
雙向關聯的本質是告訴 Hibernate 讓兩個實體共用一張資料庫表(或表結構)。
這裡以 @ManyToMany
為例(參考Hibernate User
Guide)
:有兩個實體 Person
和 Address
,一個 Person 可以擁有多個 Address,而一個
Address 也可以屬於多個 Person。於是設計實體如下:
public static class Person { private Long id; private List<Address> addresses = new ArrayList<>(); // ... omit all other stuff |
問題來了,我們應該建立一張關聯表還是兩張呢?其實取決於使用業務含義。即如果
Person
中 addresses
的含義是“人的居住地址”,而 Address
中的 owners
與之對應,表達的是“地址上居住的人”,則它們應該是一張關聯表。但如果 Address
owners
表達的是“地址的主人(如房東)”,則二者就不應該共用一張關聯表。
如何告訴 Hibernate 需要共用一張表呢?通過 mappedBy
:
public static class Person { private List<Address> addresses = new ArrayList<>(); // ... omit all other methods}public static class Address { "addresses") (mappedBy = 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
時,需要這樣實現:
public static class Person { //...omit other fields 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
欄位中;刪除時相似。“管理關聯”表示需要在程式碼級別來管理關聯雙方實體的聯絡。
如果從資料庫的角度思考,我們知道 Person
與 Address
的關係是儲存在一張關聯表裡的,一個關聯存入這張表後,不論哪一方讀取,都應該反映出新的關聯關係,而在
Hibernate 這一層,卻需要我們顯式地(從另一方的 set
)中新增/刪除這個關聯,顯得不可思議。
另外,注意我們往 set
中新增 address
或 person
時,需要我們正確的實現
Person
和 Address
的 equals
和 hashCode
方法,這是另一個坑,這裡就不深入了。
#為什麼需要手工管理
終於到了“為什麼”部分了,首先是如果不手工管理會發生什麼。考慮下面的測試:
public 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
,那即使不手工管理(同步)
關聯關係也不會有多大影響。