乐趣区

关于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 = 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长久化上下文作为应用程序和数据库之前的一级缓存,缩小对存储的调用,晋升性能。
退出移动版