JPA: Spring Data JPA @OneToMany 註解參數 orphanRemoval,一對多刪除詳解

分析了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的數據該屬性值相同(全刪全導方式會出現此場景),這種方式會有什麼問題嗎?大家可以做下實驗,我下篇文章再繼續寫。本文就到此爲止了~

 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章