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的数据该属性值相同(全删全导方式会出现此场景),这种方式会有什么问题吗?大家可以做下实验,我下篇文章再继续写。本文就到此为止了~

 
 

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