JPA:Spring Data JPA @OneToMany级联,多方删除修改新增总结(尤其删除操作:添加注解属性orphanRemoval在下一篇解释删除)

一方维护关系体现在更新多方中的外键字段。

一方在oneToMany上设置的级联保存和更新很好理解,多方会随着一方进行保存和更新。但是级联删除其实只是指一方删除时会把关联的多方数据全部删除,并不能删除一方维护的多方list中remove掉的数据。所以本文所讨论的实验和是否设置级联删除是没有关系的。

 

本文基于实验,我们先设定有如下对象,User为一方,ContactInfo为多方。每个user有多个contactInfo。

所做的操作是先查询User,然后对关联的ContactInfo做增删改。

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)
    @JoinColumn(name = "user_id")
    private List<ContactInfo> contactInfos = new ArrayList<>();
}
 
public class ContactInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    private String phoneNumber;
 
    private String address;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    @JsonIgnore
    private User user;
}

一对多关系,通过@onToMany注解实现,此注解有个属性mappedBy,这个属性默认为空(上面示例代码未设置,取默认值),代表一方要维护关系。如果mappedBy设置为一方对象的值,如mappedBy = "user",代表一方放弃维护关系,具体表现就是在插入或者删除操作的时候,一方不会去update多方的外键。这在后面的实验中会有所体现。

在讲解实验前,为了照顾没时间看完全文的读者,我先给出最终的结论:一方应放弃维护关系,由多方自行维护。这适用于绝大多数的场景。下文会详细描述整个实验过程以及如何得出的结论。

我们先看上面示例代码这种配置(不设置mappedBy),也就是一方不放弃维护关系的实验。

一方不放弃维护关系
关系配置代码

User类
@Fetch(FetchMode.SUBSELECT)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private List<ContactInfo> contactInfos = new ArrayList<>();
 
ContactInfo类
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@JsonIgnore
private User user;

实验如下

1、多方新增

 持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().add(ContactInfo.builder()
        	.address("朝阳望京街道")
        	.phoneNumber("18612938250").build());
userRepository.save(user);

JPA执行过程:

1、先插入一条userId为空的contactInfo(由于未设置user)

 insert into contact_info (address, phone_number, user_id) values (?, ?, ?)

2、然后更新userId

update contact_info set user_id=? where id=?

分析:

步骤1的insert操作是一方级联persist触发的操作。步骤2是因为一方还要维护外键,所以会对多方新增的数据update外键。

问题:

如果数据库设置了外键不能为空,那么步骤1无法执行。为了避免这个问题,可以在构造ContactInfo的时候把user对象设置进来。

2、多方更新:

持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().get(0).setPhoneNumber("88888888");
userRepository.save(user);

JPA执行过程:

1、直接根据多方主键进行更新

update contact_info set address=?, phone_number=?, user_id=? where id=?

 

分析:

因为设置了级联update,所以save user的时候会update多方contactInfo

 

3、多方删除:

A)仅从一方的list中remove

持久化代码:

User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
user.getContactInfos().remove(deletedContact);
userRepository.save(user);

JPA执行过程:

只是把deletedContact的user_Id更新为null,相当于断开了关系连接。如果您的表设计外键不能为空,则数据库报错。

update contact_info set user_id=null where user_id=? and id=?

分析:

所以从list中移除deletedContact,意味着user和此条contactInfo的关系断开了。又因为一方没有放弃关系的维护,这个操作会触发被remove掉的deletedContact的外键userId被置空。

此时去掉userRepository.save(user),什么都不会发生。这好像是废话,不过结合下面的实验对比来看,是有不同效果的。

问题:

并没有删除掉deletedContact数据,只是外键被置空。如果一方和多方是聚合关系,并且不想真正删除多方数据(多方数据可以和别的一方数据再次关联),那么适用这种方式。但如果是组合关系,那么不存在多方和一方再次关联的情况,是不适用这种方式的。

另外数据库也存在如果设置外键不能为空,不能更新的问题。

 

B)一方list中remove,并且多方显示delete

持久化代码:
 

User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
user.getContactInfos().remove(deletedContact);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

JPA执行过程:

1、remove操作把此条记录的user_id更新为null。

update contact_info set user_id=null where user_id=? and id=?

2、显式delete方法彻底删除多方的数据

delete from contact_info where id=?

 

分析:

1、更新外键为空,这是因为一方要维护关系。

2、删除多方数据,是因为显示调用了多方的delete方法。

如果我们想彻底删除掉多方的数据,这里其实做了一次无用的更新外键为空的操作。这个操作不但无用,而切一旦设置了外键不能为空,还会导致sql执行报错!

因此想彻底删除多方时,不要用这种方式(即一方不放弃维护关系)!

在这个实验中,我还做了个小测试,我把userRepository.save(user)可以去掉。发现程序正确执行,并且和去掉前的结果一样。我推断是因为此时持久化操作从多方delete发出,但是外键维护关系一方未放弃,还是会执行update的操作。

 

C)只在多方delete

持久化代码:

User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

JPA执行过程:

什么都没发生!

分析:

由于先进行了查询,所以jpa认为被删除的contactInfo数据和user的关系还在。直接删除contactInfo无效。必须先从一方持有的list中remove掉才行。

 
一方不放弃维护关系实验结论:


由于双方都维护外键关系,一方维护关系体现在对多方外键的更新上。而remove操作,只是断开关联。但不会删除多方数据。remove之后,多方显式调用delete操作,多方才会被删除。

在这种配置下,插入和删除,都会多执行一条update多方外键的sql,很多情况下是完全没必要的。而且如果数据库外键如果不能为空会报错。

适用场景:

1、多方的外键可以为空。也就是说多方和一方的关系是聚合,允许多方不关联一方。

2、只想update多方外键为空,而不想彻底删除多方数据。也就是3-A)的场景。

 

不适用场景:

1、想彻底删除多方数据,而且多方外键不能为空

 

 

一方放弃维护关系

关系配置代码

User
@Fetch(FetchMode.SUBSELECT)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user")
private List<ContactInfo> contactInfos = new ArrayList<>();

注:User中加上了mappedBy,代表user放弃维护外键关系

 

1、多方新增

A)没有给contactInfo设置user

 

持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().add(ContactInfo.builder()
        	.address("朝阳望京街道")
        	.phoneNumber("18612938250").build());
userRepository.save(user);

JPA执行过程:

只会新增一条userId为空的contactInfo

insert into contact_info (address, phone_number, user_id) values (?, ?, ?)

 

分析:

由于一方放弃维护关系,那么不会有update外键的操作。而由于设置了级联persist,所以多方数据会级联插入。但是导致插入的多方数据没有外键。如果数据库做了限制则会报错。

这种方式是错误的方式,即使成功插入也没有外键值。插入的数据和代码表述的含义不一致。

 

B)contactInfo设置user

持久化代码:
 

User user=userRepository.findById(1L).get();
user.getContactInfos().add(ContactInfo.builder()
        	.address("朝阳望京街道")
        	.phoneNumber("18612938250")
.user(user).build());
userRepository.save(user);

JPA执行过程:

新增contactInfo,user_id正常

insert into contact_info (address, phone_number, user_id) values (?, ?, ?)

分析:

1、由于一方放弃维护多方外键,所以新增的时候不会去更新外键。

2、但由于级联新增的设置,所以还是会插入多方数据。

3、多方需手动设置外键的关联对象,插入时外键才会有值。

这是一方放弃关系维护时,正确的多方插入姿势!!别忘了给插入的多方数据设置关联的一方对象

 

2、多方更新

持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().get(0).setPhoneNumber("88888888");
userRepository.save(user);

JPA执行过程:

直接根据多方主键进行更新。和一方未放弃维护关系时一致

update contact_info set address=?, phone_number=?, user_id=? where id=?

分析:

由于更新前,先进行了查询,并且配置了双向关联,所以被更新的contactInfo数据是有关联user的,因此更新正常。
 

3、多方删除

A)仅从一方的list中remove

 

JPA执行过程:

什么都没有发生

分析:

remove操作只是使关系断开。但由于一方放弃外键关系维护,所以不会更新多方外键。而由于没有显式delete多方,所以也不会删除contactInfo数据。这种删除方式显然是错误的。

B)仅在多方delete 

User user=userRepository.findById(1L).get();
 
ContactInfo deletedContact = user.getContactInfos().get(1);
//需要调用下:理解为清除对ContactInfo 表数据的引用,不然会报错关闭session或者deleted instance passed to merge:
user.getContactInfos().clear();

 
contactInfoRepository.delete(deletedContact);
 
userRepository.save(user);

JPA执行过程:

如果没有加事务的话会删除那一条数据,但是由于user与contactInfo关联关系还在,又会新增一条一样的数据进入

分析:

由于先进行了查询,所以jpa认为被删除的contactInfo和user的关系还在。直接显式删除contactInfo无效。这种删除方式也是错误的。

 

C)从一方的list中remove,并且多方显式执行delete

User user=userRepository.findById(1L).get();
 
ContactInfo deletedContact = user.getContactInfos().get(1);
 
user.getContactInfos().remove(deletedContact);
 
contactInfoRepository.delete(deletedContact);
 
userRepository.save(user);

JPA执行过程:

根据主键直接删除掉contactInfo

delete from contact_info where id=?

结论:由于一方放弃了外键关系所以维护,所以remove的时候,一方不会去更新多方外键为null。在remove后关系断开,多方显式调用delete,可以删除掉contactInfo。

这是一方放弃关系维护时,正确的多方删除姿势!!别忘了先要在一方维护的多方list中remove掉删除数据,然后多方显式调用delete。

另外,去掉userRepository.save(user),删除操作也是可以正常被触发的。
 

实验总结

我先用表格的方式呈现实验结果:

 

从上面总结可以看出,绝大多数场景下,应该采取一方放弃维护关系的方式。这避免了插入和删除时执行两条sql的问题,而且也不会因为数据库设置了外键字段不能为空,导致update的sql报错。新增时候,多方自己设置外键,一条insert语句搞定。删除时候也是一条delete语句搞定,效率更高。

只有在一方和多方是聚合关系,并且不想彻底删除多方的场景下,一方不放弃维护关系的方式才有用武之地。

其实看到最后,我们可以得出这样的结论:

一方设置mappedBy,放弃关系维护。这适用于绝大多数场景。

正确的多方新增方式:

手动在多方对象设置一方对象

正确的多方删除方式:

1、从一方维护的多方list中remove,

2、显式delete多方对象。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值