引言

问题起源于对新我的项目-数字外围的代码审查,在审阅账户模块后,发现补录、更新等接口没有调用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 = false)  // 事务不回滚public void updateCustomerInTransaction() {    this.update();}

查看数据库,更新胜利。

查看日志,在updateCustomerInTransaction办法执行完后,Hibernate执行了update CUSTOMER set name=?, phone=? where id=?更新,自动更新胜利。

2022-01-15 09:42:34.461  INFO 8206 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : ----------更新测试用例开始----------Hibernate: insert into CUSTOMER (name, phone) values (?, ?)2022-01-15 09:42:34.537  INFO 8206 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 构建测试数据: Customer{id=1, name='Hello Kitty!', phone='17712345678'}2022-01-15 09:42:34.559  INFO 8206 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 更新测试数据: Customer{id=1, name='Hello 冬泳怪鸽!', phone='18888888888'}2022-01-15 09:42:34.559  INFO 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();}

查看数据库,数据没有更新。

查看日志,Hibernate没有执行update自动更新。

2022-01-15 10:26:20.866  INFO 8897 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : ----------更新测试用例开始----------Hibernate: insert into CUSTOMER (name, phone) values (?, ?)2022-01-15 10:26:20.996  INFO 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.054  INFO 8897 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 更新测试数据: Customer{id=2, name='Hello 冬泳怪鸽!', phone='18888888888'}2022-01-15 10:26:21.054  INFO 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.461  INFO 8206 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : ----------更新测试用例开始----------②Hibernate: insert into CUSTOMER (name, phone) values (?, ?)③2022-01-15 09:42:34.537  INFO 8206 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 构建测试数据: Customer{id=1, name='Hello Kitty!', phone='17712345678'}④2022-01-15 09:42:34.559  INFO 8206 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 更新测试数据: Customer{id=1, name='Hello 冬泳怪鸽!', phone='18888888888'}⑤2022-01-15 09:42:34.559  INFO 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 = false)  // 事务不回滚public void forceUpdateCustomerInTransaction() {    this.forceUpdate();}

日志执行后果如下,强制调用了save办法,但非立刻执行,最终的update语句仍在办法完结后执行。

2022-01-15 11:38:52.810  INFO 9512 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : ----------强制更新测试用例开始----------Hibernate: insert into CUSTOMER (name, phone) values (?, ?)2022-01-15 11:38:52.914  INFO 9512 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 构建测试数据: Customer{id=3, name='Hello Kitty!', phone='17712345678'}2022-01-15 11:38:52.943  INFO 9512 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 更新测试数据: Customer{id=3, name='Hello 冬泳怪鸽!', phone='18888888888'}2022-01-15 11:38:52.943  INFO 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.616  INFO 9977 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : ----------强制更新测试用例开始----------Hibernate: insert into CUSTOMER (name, phone) values (?, ?)2022-01-15 12:29:27.721  INFO 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.801  INFO 9977 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : 更新测试数据: Customer{id=4, name='Hello 冬泳怪鸽!', phone='18888888888'}2022-01-15 12:29:27.801  INFO 9977 --- [           main] c.s.q.s.r.CustomerRepositoryTest         : ----------强制更新测试用例完结----------

总结

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