JPA|JPA 实体脏检查与存储同步(Dirty & Flush)
引言
问题起源于对新项目-数字核心的代码审查,在审阅账户模块后,发现补录、更新等接口没有调用JPA
的仓库save
方法进行数据持久化,但更新依然生效,随查阅资料文献,开启了对本议题的探究。
目标:没有调用save
方法,更新是怎么生效的?
试一试
在查阅大量资料后,了解到与JPA
持久化上下文Persistence Context
有关,一起试试吧。
实验准备
初始化spring-boot
项目,依赖spring-data-jpa
,并开启spring-data
的show-sql
配置以便调试。
spring:
jpa:
show-sql: true
建立客户信息实体:
/**
* 客户信息表
*/
@Entity
@Table(name = "CUSTOMER")
public class Customer {@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 客户姓名
*/
private String name;
/**
* 客户手机号
*/
private String phone;
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
配置
DataJpaTest
启用JPA
测试环境,不启动整个spring-context
,可以减少单元测试执行耗时。/**
* 客户信息仓库测试
*/
@DataJpaTest// 自动配置JPA测试
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)// 不启用内嵌数据库替代
public class CustomerRepositoryTest {private static final Logger logger = LoggerFactory.getLogger(CustomerRepositoryTest.class);
}
复现更新场景 更新方法如下,构建一条
Hello Kitty!
测试数据,并对保存后的实体信息进行修改,但不调用save
。/**
* 数据更新方法
*/
public void update() {
logger.info("----------更新测试用例开始----------");
Customer customer = new Customer();
customer.setName("Hello Kitty!");
customer.setPhone("17712345678");
this.customerRepository.save(customer);
logger.info("构建测试数据: {}", customer);
Long id = customer.getId();
this.customerRepository.findById(id).ifPresent(entity -> {
entity.setName("Hello 冬泳怪鸽!");
entity.setPhone("18888888888");
logger.info("更新测试数据: {}", entity);
});
logger.info("----------更新测试用例结束----------");
}
开启事务,设置事务不回滚,调用上文的
update
方法。@Test
@Transactional// 开启事务
@Rollback(value = https://www.it610.com/article/false)// 事务不回滚
public void updateCustomerInTransaction() {
this.update();
}
查看数据库,更新成功。
文章图片
查看日志,在
updateCustomerInTransaction
方法执行完后,Hibernate
执行了update CUSTOMER set name=?, phone=? where id=?
更新,自动更新成功。2022-01-15 09:42:34.461INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------更新测试用例开始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 09:42:34.537INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: 构建测试数据: Customer{id=1, name='Hello Kitty!', phone='17712345678'}
2022-01-15 09:42:34.559INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: 更新测试数据: Customer{id=1, name='Hello 冬泳怪鸽!', phone='18888888888'}
2022-01-15 09:42:34.559INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------更新测试用例结束----------
Hibernate: update CUSTOMER set name=?, phone=? where id=?
关闭事务,对比实验。
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)// 挂起/关闭事务
public void updateCustomerWithoutTransaction() {
this.update();
}
查看数据库,数据没有更新。
文章图片
【JPA|JPA 实体脏检查与存储同步(Dirty & Flush)】查看日志,
Hibernate
没有执行update
自动更新。2022-01-15 10:26:20.866INFO 8897 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------更新测试用例开始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 10:26:20.996INFO 8897 --- [main] c.s.q.s.r.CustomerRepositoryTest: 构建测试数据: Customer{id=2, name='Hello Kitty!', phone='17712345678'}
Hibernate: select customer0_.id as id1_0_0_, customer0_.name as name2_0_0_, customer0_.phone as phone3_0_0_ from CUSTOMER customer0_ where customer0_.id=?
2022-01-15 10:26:21.054INFO 8897 --- [main] c.s.q.s.r.CustomerRepositoryTest: 更新测试数据: Customer{id=2, name='Hello 冬泳怪鸽!', phone='18888888888'}
2022-01-15 10:26:21.054INFO 8897 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------更新测试用例结束----------
对比实验结果:事务开启的前提下,对实体的改动会自动持久化到数据库;当事务关闭时,则不生效。
持久化上下文 我们先来了解下
JPA
持久化上下文:The persistence context is the first-level cache where all the entities are fetched from the database or saved to the database. It sits between our application and persistent storage.具体分析下事务状态下实验的日志:
Persistence context keeps track of any changes made into a managed entity. If anything changes during a transaction, then the entity is marked as dirty. When the transaction completes, these changes are flushed into persistent storage.
If every change made in the entity makes a call to persistent storage, we can imagine how many calls will be made. This will lead to a performance impact because persistent storage calls are expensive.
持久化上下文是一级缓存,缓存中所有实体都是从数据库中fetch
或save
到数据库中的,它位于应用程序和持久存储之间。
持久化上下文跟踪所管理实体的所有更改,如果在事务中发生改变,实体会被标记为dirty
,事务完成后,所有改动会同步到持久存储。
如果实体做的每一次改动都要调用存储,可以想象需要将调用很多次,这会引起性能问题,因为持久存储调用很昂贵。
①2022-01-15 09:42:34.461INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------更新测试用例开始----------
②Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
③2022-01-15 09:42:34.537INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: 构建测试数据: Customer{id=1, name='Hello Kitty!', phone='17712345678'}
④2022-01-15 09:42:34.559INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: 更新测试数据: Customer{id=1, name='Hello 冬泳怪鸽!', phone='18888888888'}
⑤2022-01-15 09:42:34.559INFO 8206 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------更新测试用例结束----------
⑥Hibernate: update CUSTOMER set name=?, phone=? where id=?
- ① 方法开始
- ②
CUSTOMER
表插入数据,并将该实体保存到持久化上下文 - ③ 打印持久化后的数据
- ④ 更新实体数据,这里没有执行
select
语句,因为持久化上下文存在该实体,findById
直接从持久化上下文中获取 - ⑤ 方法结束
- ⑥ 事务提交前,检查实体为
Dirty
,改动同步到数据库
JPA
持久化上下文中强制调用save
会发生什么?修改更新方法为强制更新,在修改
entity
后手动调用save
方法更新。/**
* 数据强制更新方法
*/
public void forceUpdate() {
logger.info("----------强制更新测试用例开始----------");
Customer customer = new Customer();
customer.setName("Hello Kitty!");
customer.setPhone("17712345678");
this.customerRepository.save(customer);
logger.info("构建测试数据: {}", customer);
Long id = customer.getId();
this.customerRepository.findById(id).ifPresent(entity -> {
entity.setName("Hello 冬泳怪鸽!");
entity.setPhone("18888888888");
this.customerRepository.save(entity);
logger.info("更新测试数据: {}", entity);
});
logger.info("----------强制更新测试用例结束----------");
}
开启事务,设置事务不回滚,调用上文的
forceUpdate
方法。@Test
@Transactional// 开启事务
@Rollback(value = https://www.it610.com/article/false)// 事务不回滚
public void forceUpdateCustomerInTransaction() {
this.forceUpdate();
}
日志执行结果如下,强制调用了
save
方法,但非立即执行,最终的update
语句仍在方法结束后执行。2022-01-15 11:38:52.810INFO 9512 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------强制更新测试用例开始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 11:38:52.914INFO 9512 --- [main] c.s.q.s.r.CustomerRepositoryTest: 构建测试数据: Customer{id=3, name='Hello Kitty!', phone='17712345678'}
2022-01-15 11:38:52.943INFO 9512 --- [main] c.s.q.s.r.CustomerRepositoryTest: 更新测试数据: Customer{id=3, name='Hello 冬泳怪鸽!', phone='18888888888'}
2022-01-15 11:38:52.943INFO 9512 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------强制更新测试用例结束----------
Hibernate: update CUSTOMER set name=?, phone=? where id=?
关闭事务,对比实验。
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)// 挂起/关闭事务
public void forceUpdateCustomerWithoutTransaction() {
this.forceUpdate();
}
日志如下,多执行了一个
select + update
,猜测JPA
不开启事务的情况下,先查询当前实体信息和数据库记录是否有变化,有变化则进行更新。2022-01-15 12:29:27.616INFO 9977 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------强制更新测试用例开始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 12:29:27.721INFO 9977 --- [main] c.s.q.s.r.CustomerRepositoryTest: 构建测试数据: Customer{id=4, name='Hello Kitty!', phone='17712345678'}
Hibernate: select customer0_.id as id1_0_0_, customer0_.name as name2_0_0_, customer0_.phone as phone3_0_0_ from CUSTOMER customer0_ where customer0_.id=?
Hibernate: select customer0_.id as id1_0_0_, customer0_.name as name2_0_0_, customer0_.phone as phone3_0_0_ from CUSTOMER customer0_ where customer0_.id=?
Hibernate: update CUSTOMER set name=?, phone=? where id=?
2022-01-15 12:29:27.801INFO 9977 --- [main] c.s.q.s.r.CustomerRepositoryTest: 更新测试数据: Customer{id=4, name='Hello 冬泳怪鸽!', phone='18888888888'}
2022-01-15 12:29:27.801INFO 9977 --- [main] c.s.q.s.r.CustomerRepositoryTest: ----------强制更新测试用例结束----------
总结
- 开启事务,
JPA
持久化上下文在事务提交时进行实体脏检查,并同步到数据库。 JPA
持久化上下文作为应用程序和数据库之前的一级缓存,减少对存储的调用,提升性能。
推荐阅读
- 病理预测
- 适合写进作文与疫情相关的句子|适合写进作文与疫情相关的句子|你看 爱和希望蔓延的比病毒更快 每一种爱 都刻进武汉的心脏
- 随笔|随笔 心累了
- 为什么肾脏会得结石()
- 塑封书越来越多,缺少了看书体验,你还愿意实体店买书吗()
- 数据库事务与锁
- 这副画画了好久好久,完工对我来说真的好不容易,心底不知给自己说了多少次别前功尽气,画面很多脏颜料,还好电脑功能强大,处理掉了,色彩也调了
- 诺丽果(修复肝细胞,增强肝脏活力!)
- 实体店商家通过微信小程序解决流量和租金等问题-小程序登录小程序源码
- 做销售的人的真实体会(92)