分析了OneToMany級聯操作多方的插入、更新、刪除。我們得到如下結論:
1、插入,建議一方設置mappedBy,好處是隻會執行一條insert語句。不會執行多餘的update外鍵的sql。
2、更新,沒有區別
3、刪除,一方設置mappedBy。一方維護的多方集合remove,多方顯示刪除。
orphanRemoval
插入和更新都沒有什麼問題。但是刪除就有些奇怪了,一方和多方均要操作,如果看過前面文章分析,倒也是合情合理。但操作起來實在是麻煩,今天codereview時,研發小夥伴們也提出疑問。如果一方通過對多方集合的remove操作即觸發刪除(無需多方顯式刪除),那就方便多了,而且直觀好理解。可惜通過前文的實驗,發現設置了mappedBy,單純的在集合中remove不會有任何效果;不設置mappedBy,集合中remove只會把多方的外鍵update爲null。並不能達到刪除的目的。
難道真的不行?我又打開OneToMany的代碼,發現這麼一個屬性:
/**
* (Optional) Whether to apply the remove operation to entities that have
* been removed from the relationship and to cascade the remove operation to
* those entities.
* @since Java Persistence 2.0
*/
boolean orphanRemoval() default false;
看註釋,說的很明白,如果設置爲true,當關系被斷開時,多方實體將被刪除。這不正是我們想要的效果嘛?!這麼重要的參數之前怎麼沒注意到呢?回想一下,其實之前我也是試過這個參數的,但是印象中並沒有達到我想要的效果,應該是沒有執行刪除的操作。但是這黑紙白字寫的清清楚楚的註釋,不應該啊。。。。
我決定再測試一次,於是修改了我的配置,加上 orphanRemoval=true :
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String userName;
private String password;
@Fetch(FetchMode.SUBSELECT)
@OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY, orphanRemoval=true, mappedBy = "user")
private List<ContactInfo> contactInfos = new ArrayList<>();
}
持久化代碼:
User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
user.getContactInfos().remove(deletedContact);
userRepository.save(user);
持久化代碼只在一方維護的list中remove掉想刪除的數據
運行程序,看到如下sql輸出:
Hibernate: delete from contact_info where id=?
成功了!!
這是怎麼回事呢?我清楚記得之前測試過這個設置,但沒能刪除成功。
犯的錯誤
難道是之前沒有設置mappedBy嗎?於是我去掉mappedBy再次測試下,配置如下:
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String userName;
private String password;
@Fetch(FetchMode.SUBSELECT)
@OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY, orphanRemoval=true)
@JoinColumn(name = "user_id")
private List<ContactInfo> contactInfos = new ArrayList<>();
}
運行下,打印如下sql:
Hibernate: update contact_info set user_id=null where user_id=? and id=?
然後報錯了:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'user_id' cannot be null
這是因爲沒有設置mappedBy,所以在liste中remove的操作,一方要維護關係,體現在update多方外鍵爲空。但因爲數據庫設置了not null的約束,所以報錯。
真相大白
其實到現在真相已經水落石出了。由於之前我使用orphanRemoval時,沒有設置mappedBy,所以先執行了update語句(一方要維護關係)。但因爲沒有刪除動作發生,並且程序報錯,我錯誤的認爲orphanRemoval=true沒有效果。JPA代碼註釋裏也沒有提到和mappedBy的關係,也是導致當時我判斷錯誤的原因之一。
不過註釋沒提到這一點,現在我也是理解的,因爲如果外鍵沒有設置not null,在update之後是會執行delete的(已經測試過)。orphanRemoval和mappedBy本來就是兩個相互獨立的屬性,每人負責自己的事情,但是搭配不當就會產生意料之外的效果,並且很容易讓人產生誤會。JPA坑就坑在這裏,不過搞懂每個屬性作用,以及執行的順序後,我們運用起來就自如多了。
到這裏還沒有完,我提個問題:
設置了orphanRemoval=true,導致級聯刪除。這和oneToMany配置的cascade有關係嗎?
答案是沒有關係,我們設置cascade = {CascadeType.PERSIST,CascadeType.MERGE},不設置REMOVE操作,發現一樣可以通過從集合中remove來刪除多方。這裏再次印證了上篇文章開頭我說的:CascadeType.REMOVE只是指刪除一方,是否把關聯多方全部刪除。
總結
我們再聊下mappedBy和orphanRemoval,在一方維護的list中remove掉多方時產生的效果。
之前我犯的錯誤就是未設置mappedBy,但設置了orphanRemoval。但是JPA會先執行mappedBy未設置產生的update語句,導致not null報錯。而我錯誤的認爲這是orphanRemoval引起的(或者說沒產生刪除效果)。
本篇文章到這裏就結束了,我們得出一個結論,如果想通過對一方維護的多方集合做remove操作,就達到刪除多方數據的效果,那麼需要同時設置 orphanRemoval=true, mappedBy = "一方對象"。如果不設置orphanRemoval=true,那麼需要額外顯式刪除多方對象。
結束語
其實所有看到的都是表象,我們記住那麼多實驗結果不可能也沒有必要。我們只需要記住每個屬性的設置分別產生什麼效果就好。如果這個都記不住,那就記最重要的orphanRemoval=true, mappedBy = "一方對象"。不過這個真的是萬能的嗎?
懸念
如果按照前文的配置方式,在一方維護的list上既add也remove,理論上會既插入又刪除。我做了測試也證明確實如此。但是會有什麼潛在問題嗎?假如數據庫設置了某個字段的唯一性約束,remove掉的數據和insert的數據該屬性值相同(全刪全導方式會出現此場景),這種方式會有什麼問題嗎?大家可以做下實驗,我下篇文章再繼續寫。本文就到此爲止了~