Spring Data JPA OneToMany级联,多方删除修改新增详解(好文章!!申精!!)
前言
近期的项目中使用Spring Data JPA。JPA带来很大的便捷,但它内部映射关系及持久化机制如果理解不到位会出现很多问题。不同的配置将会产生不同的执行过程。如果不了解其运行机制,很容易在一个问题上摸索很久,找不到答案。近期碰到一个问题,在一对多关系中,先进行了一方的查询,然后找到需要删除多方数据,做删除操作。看似简单的删除,但JPA在不同的onToMany配置下,却呈现出不同的执行结果。正好借此机会做了oneToMany不同配置的实验,在此做个记录。也希望通过实验,找到不同场景下最佳的配置方式。
进入正文前,我先啰嗦一下级联操作。一方在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")@JsonIgnoreprivate 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
User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
user.getContactInfos().remove(deletedContact);
userRepository.save(user);
JPA执行过程:
什么都没有发生
分析:
remove操作只是使关系断开。但由于一方放弃外键关系维护,所以不会更新多方外键。而由于没有显式delete多方,所以也不会删除contactInfo数据。这种删除方式显然是错误的。
B)仅在多方delete
User user=userRepository.findById(1L).get();ContactInfo deletedContact = user.getContactInfos().get(1);contactInfoRepository.delete(deletedContact);userRepository.save(user);
JPA执行过程:
什么都没有发生
分析:
由于先进行了查询,所以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),删除操作也是可以正常被触发的。
补充:
还可以这样:
User user=userRepository.findById(1L).get();//取得除了[1]之外剩下的,这里假设只有[0][1]
ContactInfo deletedContact0 = user.getContactInfos().get(0);user.getContactInfos().clear();user.getContactInfos().addAll(deletedContact0);userRepository.save(user);
实验总结
我先用表格的方式呈现实验结果:
从上面总结可以看出,绝大多数场景下,应该采取一方放弃维护关系的方式。这避免了插入和删除时执行两条sql的问题,而且也不会因为数据库设置了外键字段不能为空,导致update的sql报错。新增时候,多方自己设置外键,一条insert语句搞定。删除时候也是一条delete语句搞定,效率更高。
只有在一方和多方是聚合关系,并且不想彻底删除多方的场景下,一方不放弃维护关系的方式才有用武之地。
其实看到最后,我们可以得出这样的结论:
一方设置mappedBy,放弃关系维护。这适用于绝大多数场景。
正确的多方新增方式:
手动在多方对象设置一方对象
正确的多方删除方式:
1、从一方维护的多方list中remove,
2、显式delete多方对象。
————————————————
原文链接,对原文有补充:https://blog.csdn.net/liyiming2017/article/details/90218062
Spring Data JPA OneToMany级联,多方删除修改新增详解(好文章!!申精!!)相关推荐
- Spring Data JPA OneToMany注解参数orphanRemoval,一对多删除详解
博主:爱码叔 个人博客站点: icodebook 公众号:漫话软件设计 专注于软件设计与架构.技术管理.擅长用通俗易懂的语言讲解技术.对技术管理工作有自己的一定见解.文章会第一时间首发在个站上,欢迎大 ...
- JPA: Spring Data JPA @OneToMany 注解参数 orphanRemoval,一对多删除详解
分析了OneToMany级联操作多方的插入.更新.删除.我们得到如下结论: 1.插入,建议一方设置mappedBy,好处是只会执行一条insert语句.不会执行多余的update外键的sql. 2.更 ...
- 【Spring Data JPA自学笔记五】一对多、多对多和级联
文章目录 数据库表的关系 一对多 多对多 Spring Data JPA实现一对多 基本配置 实现一对多 放弃维护权 Spring Data JPA实现多对多 基本配置 实现多对多 级联 之前的所有操 ...
- 解决Spring data jpa 批量插入/删除(saveAll()/deleteAll())速度慢的问题
问题描述: 项目中使用到了Spring data jpa技术,调用 JpaRepository.saveAll()/deleteAll()方法对list中的数据进行插入/删除时,发现速度特别慢,数据量 ...
- JPA Spring Data JPA详解
JPA & Spring Data JPA 一.JPA 1. JPA是什么 JPA(Java Persistence API)Java持久化 API,是一套基于ORM思想的规范. ORM(Ob ...
- Spring Data JPA多表操作(5)
Spring Data JPA多表操作(5) 数据库中多表之间的关系 多对多 一对多 一对一 一对多示例 数据库设计示例 实体示例 客户:指的是一家公司,我们记为A. 联系人:指的是A公司中的员工. ...
- SpringBoot学习笔记:Spring Data Jpa的使用
更多请关注公众号 Spring Data Jpa 简介 JPA JPA(Java Persistence API)意即Java持久化API,是Sun官方在JDK5.0后提出的Java持久化规范(JSR ...
- hql实例 jpa_SpringBoot学习笔记九:Spring Data Jpa的使用
Spring Data Jpa 简介 JPA JPA(Java Persistence API)意即Java持久化API,是Sun官方在JDK5.0后提出的Java持久化规范(JSR 338,这些接口 ...
- ORM框架之Spring Data JPA(三)高级查询---复杂查询
一.spring data jpa高级查询 1.1Specifications动态查询 有时我们在查询某个实体的时候,给定的条件是不固定的,这时就需要动态构建相应的查询语句,在Spring Data ...
最新文章
- 构造函数的初始化列表
- python处理excel表格数据-零基础使用Python读写处理Excel表格的方法
- 【python教程】对多线程中join()的详细教程
- 高等数学回顾.pptx
- TensorFlow学习笔记(十一)读取自己的数据进行训练
- 《Python 黑科技》程序员必须会的代理ip小技巧
- Javascript实现计数器,定时警告和停止
- iphone7p配置参数详情_华为mate40标准版参数配置-参数详情
- python 自动登录网站_python实现网站用户名密码自动登录功能
- 数据库系统概论完整笔记
- SoundPool详解
- 2022考研数二解答题规范给分(17,18,19,22)
- Windows10系统 定时开/关机设置
- DHCP动态获取IP地址流程
- win11最新bug修复合集(来源于微软官方)
- hadoop集群-单词统计
- 华为平板 M3(青春版)ROOT教程 华为平板 M3一键root步骤
- 0x01-medium_socnet
- 软件测试过程中的文档内容
- 专用小交换机(PBX)的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告