spring data jpa cascade级联操作研究
由于平时用mybatis比较多,刚接触spring data jpa的时候,对一对多,多对多关联映射以及关联之间的级联操作学的很迷糊,于是自己实验总结了一下spring data jpa中的6种级联操作。
spring data jpa的级联操作有如下6种CascadeType.ALL,CascadeType.DETACH,CascadeType.MERGE,CascadeType.PERSIST,CascadeType.REFRESH,CascadeType.REMOVE。
其中ALL代表包含所有其他5种,所以我们只需要研究其他5种即可。
对于使用的测试代码,后文只列出了关键代码,想要自己动手尝试的小伙伴可以从以下地址下载代码。
https://gitee.com/zzjzzy/spring-data-jpa-study
先说一下测试用到的表
数据库有两张表,如下,user表和department表为多对一,一个用户属于一个部门,一个部门可以有多个用户,在用户表中用dept_id维护关联关系。
两张表的主键都为int,自增。
表所对应的实体类分别为User和Department,具体代码比较长,就不贴出来了,感兴趣的可以去上面的仓库地址查看。
这里只列出User实体中比较重要的一部分代码如下,User实体中有一个department字段,用来维护关联关系,Department实体就是普通的java bean,没有维护关联关系。
@ManyToOne(targetEntity = Department.class, cascade = CascadeType.REFRESH, fetch = FetchType.EAGER)
@JoinColumn(name = "dept_id", referencedColumnName = "id")
private Department department;
1. CascadeType.PERSIST
persist是持久化的意思,所以这个级联操作的意思是在我保存user表数据时,如果User实体中有department信息,会把department信息也级联保存了。
在做测试之前,我们的数据库初始状态如下:
关键测试代码如下:
Department department = new Department();
department.setCode("YXB");
department.setName("营销部");
User user = new User();
user.setDepartment(department);
user.setName("zzj");
repository.save(user);
测试结果如下:
cascade | 结果 |
CascadeType.ALL; | 同PERSIST |
CascadeType.DETACH; | 报错 org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.example.demo.User.department -> com.example.demo.Departmen |
CascadeType.MERGE; | 报错,同上 |
CascadeType.PERSIST; | user表新增一条记录 同时department表新增一条YXB, 营销部的记录 |
CascadeType.REFRESH; | 报错,同上 |
CascadeType.REMOVE; | 报错,同上 |
可以看到,只有ALL和PERSIST成功保存了User实体中携带的department信息。
2. CascadeType.MERGE
merge意思是合并的意思,就是当User实体中的department数据有更新时,保存user时也会更新department信息,注意是更新,如果User实体中持有的是一个数据库中没有的department,是会像上面一样报错的。
测试前数据库初始状态:同上
测试关键代码
Department department = new Department();
department.setId(9);
department.setCode("YXB");
department.setName("营销部");
User user = new User();
user.setDepartment(department);
user.setName("zzj");
repository.save(user);
测试结果:
cascade | 结果 |
CascadeType.ALL; | 同MERGE |
CascadeType.DETACH; | user表新增一条dept_id为9的记录 department表数据不变 |
CascadeType.MERGE; | user表新增一条dept_id为9的记录 同时department表数据更新为YXB, 营销部 |
CascadeType.PERSIST; | user表新增一条dept_id为9的记录 department表数据不变 |
CascadeType.REFRESH; | user表新增一条dept_id为9的记录 department表数据不变 |
CascadeType.REMOVE; | user表新增一条dept_id为9的记录 department表数据不变 |
3. CascadeType.REMOVE
remove就是删除的意思,就是在删除user表数据时会关联删除department表数据,但是这就分两种情况,比如我要删除一个user,这个user的dept_id为9,那么程序就会尝试删除department表中id为9的数据,但是如果user表还有其他记录的dept_id也为9,那么把id为9的department记录删除就是有问题的,下面我们来测试一下spring data jpa的表现。
测试前数据库状态
测试关键代码
Department department = new Department();
department.setId(9);
User user = new User();
user.setId(1);
user.setDepartment(department);
repository.delete(user);
测试结果
cascade | 结果 |
CascadeType.ALL; | 同REMOVE |
CascadeType.DETACH; | user表记录被删除 department表没有 |
CascadeType.MERGE; | user表记录被删除 department表没有 |
CascadeType.PERSIST; | user表记录被删除 department表没有 |
CascadeType.REFRESH; | user表记录被删除 department表没有 |
CascadeType.REMOVE; | user表记录被删除 department表也被删除 |
接下来我们把user表的初始状态改为如下状态,也就是有两个user的dept_id都为9
测试结果,测试结果要分如下两种情况,注意区分。
cascade | 数据库设置了外键约束 | 数据库没有设置外键约束 |
CascadeType.ALL; | 同REMOVE | 同REMOVE |
CascadeType.DETACH; | user表记录被删除 department表没有 |
user表记录被删除 department表没有 |
CascadeType.MERGE; | user表记录被删除 department表没有 |
user表记录被删除 department表没有 |
CascadeType.PERSIST; | user表记录被删除 department表没有 |
user表记录被删除 department表没有 |
CascadeType.REFRESH; | user表记录被删除 department表没有 |
user表记录被删除 department表没有 |
CascadeType.REMOVE; | 报错:java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`test`.`user`, CONSTRAINT `FKmf82od1cs7u7drq5eua8ukyrw` FOREIGN KEY (`dept_id`) REFERENCES `department` (`id`)) |
user表记录被删除 department表也被删除 |
4. CascadeType.REFRESH
refresh是级联刷新的意思,这里要理解一下什么是刷新,java定义的jpa规范中有个EntityManager接口,里面有个EntityManager.refresh(Object entity);的方法,调用这个方法时会从数据库重新读取数据,更新entity实体的数据。用我们的例子来解释就是,当我设置了级联类型为REFRESH时,我对user调用refresh方法时,user中的department字段信息也会从数据库中读取最新数据进行更新。如果没有设置REFRESH级联类型,就只会更新user实体的信息,department字段的信息是不会更新的。
这个要做测试的话会麻烦一些,因为我们要在程序中注入EntityManager的一个实例,同时要对UserRepository开放一个refresh方法,然后调用repository.refresh(user)方法来进行测试。
具体如何在spring中注入一个EntityManager不是本文的重点了,这里贴一个链接,感兴趣的可以研究一下。https://dzone.com/articles/accessing-the-entitymanager-from-spring-data-jpa
如果不想研究如何注入EntityManager,那么在看后面的代码时你只需要知道,在调用reposity.refresh(user)时,就是要从数据库重新读取最新的数据,对user信息进行更新即可。
同样,先贴出来测试之前数据库初始状态
测试关键代码,由于这个复杂一点,所以说一下大概的测试思路。
首先我们会在数据库中查询一次user以及user关联的department信息,然后查询完后让程序停住(可以通过断点调试或Thread.spleep()),然后我们把数据库中user表name字段从“张三”手动修改为“李四”,department表的name字段从“综合部”改为"综合部门",然后放行程序,让程序调用refresh方法对User实体进行刷新,查看刷新后的user以及user中department字段值的变化。
代码中还有很多要注意的地方,具体可以看代码中注释。
//如果我们不开启事务,那么第一次查询完后session就关闭了,再调用EntityManager.refresh()方法会报错,因为session已经关闭了。所以在测试之前要开启事务。
//开启事务后还要调整事务隔离级别为读已提交,因为spring默认事务是可重复读,由于我们是在同一个事务中两次查询,以查看refresh的效果,在可重复读的隔离级别下,即使我们修改了数据库的数据,两次查询的结果也肯定是一样的,也就是我们的修改对当前事务是不可见的,refresh的效果也就看不出来了。
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
Optional<User> byId = repository.findById(1);
User user = byId.get();
System.out.println(user);
repository.refresh(user); //在此处打断点,修改数据库数据后再向下执行
System.out.println(user);
transactionManager.commit(transaction);
测试结果,可以看出,只有在REFRESH开启时,调用refresh方法时才会级联更新department的信息。
cascade | 结果 |
CascadeType.ALL; | 同REFRESH |
CascadeType.DETACH; | |
CascadeType.MERGE; | 同DETACH
|
CascadeType.PERSIST; | 同DETACH |
CascadeType.REFRESH; |
|
CascadeType.REMOVE; | 同DETACH |
5. CascadeType.DETACH
detach的意思就是脱离关系,也是就user的任何操作都不会级联到department,从上面的四个测试也可以看出detach的结果了,所以这里就不再测试了。
总结
从以上测试我们可以看出,除了detach,其他4个级联类型其实正好对应了增删改查4个操作。
persist对级联新增有影响,remove对级联删除有影响,merge对级联修改有影响,refresh对级联查询有影响。