JPA|JPA 实体脏检查与存储同步(Dirty & Flush)

引言 问题起源于对新项目-数字核心的代码审查,在审阅账户模块后,发现补录、更新等接口没有调用JPA的仓库save方法进行数据持久化,但更新依然生效,随查阅资料文献,开启了对本议题的探究。
目标:没有调用save方法,更新是怎么生效的?
试一试 在查阅大量资料后,了解到与JPA持久化上下文Persistence Context有关,一起试试吧。
实验准备 初始化spring-boot项目,依赖spring-data-jpa,并开启spring-datashow-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(); }

查看数据库,更新成功。
JPA|JPA 实体脏检查与存储同步(Dirty & Flush)
文章图片

查看日志,在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)
文章图片

【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.
持久化上下文是一级缓存,缓存中所有实体都是从数据库中fetchsave到数据库中的,它位于应用程序和持久存储之间。
持久化上下文跟踪所管理实体的所有更改,如果在事务中发生改变,实体会被标记为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: ----------强制更新测试用例结束----------

总结
  1. 开启事务,JPA持久化上下文在事务提交时进行实体脏检查,并同步到数据库。
  2. JPA持久化上下文作为应用程序和数据库之前的一级缓存,减少对存储的调用,提升性能。

    推荐阅读