前言

近期的项目中使用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级联,多方删除修改新增详解(好文章!!申精!!)相关推荐

  1. Spring Data JPA OneToMany注解参数orphanRemoval,一对多删除详解

    博主:爱码叔 个人博客站点: icodebook 公众号:漫话软件设计 专注于软件设计与架构.技术管理.擅长用通俗易懂的语言讲解技术.对技术管理工作有自己的一定见解.文章会第一时间首发在个站上,欢迎大 ...

  2. JPA: Spring Data JPA @OneToMany 注解参数 orphanRemoval,一对多删除详解

    分析了OneToMany级联操作多方的插入.更新.删除.我们得到如下结论: 1.插入,建议一方设置mappedBy,好处是只会执行一条insert语句.不会执行多余的update外键的sql. 2.更 ...

  3. 【Spring Data JPA自学笔记五】一对多、多对多和级联

    文章目录 数据库表的关系 一对多 多对多 Spring Data JPA实现一对多 基本配置 实现一对多 放弃维护权 Spring Data JPA实现多对多 基本配置 实现多对多 级联 之前的所有操 ...

  4. 解决Spring data jpa 批量插入/删除(saveAll()/deleteAll())速度慢的问题

    问题描述: 项目中使用到了Spring data jpa技术,调用 JpaRepository.saveAll()/deleteAll()方法对list中的数据进行插入/删除时,发现速度特别慢,数据量 ...

  5. JPA Spring Data JPA详解

    JPA & Spring Data JPA 一.JPA 1. JPA是什么 JPA(Java Persistence API)Java持久化 API,是一套基于ORM思想的规范. ORM(Ob ...

  6. Spring Data JPA多表操作(5)

    Spring Data JPA多表操作(5) 数据库中多表之间的关系 多对多 一对多 一对一 一对多示例 数据库设计示例 实体示例 客户:指的是一家公司,我们记为A. 联系人:指的是A公司中的员工. ...

  7. SpringBoot学习笔记:Spring Data Jpa的使用

    更多请关注公众号 Spring Data Jpa 简介 JPA JPA(Java Persistence API)意即Java持久化API,是Sun官方在JDK5.0后提出的Java持久化规范(JSR ...

  8. hql实例 jpa_SpringBoot学习笔记九:Spring Data Jpa的使用

    Spring Data Jpa 简介 JPA JPA(Java Persistence API)意即Java持久化API,是Sun官方在JDK5.0后提出的Java持久化规范(JSR 338,这些接口 ...

  9. ORM框架之Spring Data JPA(三)高级查询---复杂查询

    一.spring data jpa高级查询 1.1Specifications动态查询 有时我们在查询某个实体的时候,给定的条件是不固定的,这时就需要动态构建相应的查询语句,在Spring Data ...

最新文章

  1. 构造函数的初始化列表
  2. python处理excel表格数据-零基础使用Python读写处理Excel表格的方法
  3. 【python教程】对多线程中join()的详细教程
  4. 高等数学回顾.pptx
  5. TensorFlow学习笔记(十一)读取自己的数据进行训练
  6. 《Python 黑科技》程序员必须会的代理ip小技巧
  7. Javascript实现计数器,定时警告和停止
  8. iphone7p配置参数详情_华为mate40标准版参数配置-参数详情
  9. python 自动登录网站_python实现网站用户名密码自动登录功能
  10. 数据库系统概论完整笔记
  11. SoundPool详解
  12. 2022考研数二解答题规范给分(17,18,19,22)
  13. Windows10系统 定时开/关机设置
  14. DHCP动态获取IP地址流程
  15. win11最新bug修复合集(来源于微软官方)
  16. hadoop集群-单词统计
  17. 华为平板 M3(青春版)ROOT教程 华为平板 M3一键root步骤
  18. 0x01-medium_socnet
  19. 软件测试过程中的文档内容
  20. 专用小交换机(PBX)的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告

热门文章

  1. Python: Mediator Pattern
  2. 取二进制(非符号位)的最高位1
  3. 介绍一个好用的网盘MEGA
  4. 国产芯片--芯旺微--KungFu--ChipOn-脱机烧录
  5. fwPlayer 支持最新浏览器在线播放AVI和FLV格式的视频
  6. 不懂数学,照样做数据科学家
  7. 如何听节拍器_钢琴练习中节拍器的使用
  8. 人力资源主管的素质要求
  9. 各大搜索引擎下拉词长尾词API接口
  10. 【SAP Abap】SAP第四代增强开发DEMO