Spring集成测试中@Transactional下JPA实体本地同步问题咨询
这绝对是JPA/Hibernate的预期行为,核心原因在于Hibernate的持久化上下文(也就是JPA的EntityManager,对应Hibernate的Session)的一级缓存机制,再加上事务上下文的传播特性共同作用的结果,咱们一步步拆解:
1. 持久化上下文的一级缓存:实体实例的「跟踪器」
当你通过JpaRepository读取@Entity对象时,Hibernate会把这个实体实例存入当前线程绑定的Session一级缓存中。只要这个Session没有被关闭、清除,Hibernate就会持续跟踪这个实例的状态变化。
你的整个测试方法被@Transactional注解包裹,意味着从步骤1到步骤3,全程都处于同一个持久化上下文中。而步骤2调用的Service级事务,如果用的是默认的REQUIRED传播属性,它并不会开启一个全新的独立事务,而是加入到当前测试的事务上下文里——相当于所有操作都共享同一个Session。
2. 自动同步的本质:持久化上下文的快照同步
在同一个Session范围内,不管哪个方法完成了数据库的实体更新,Hibernate都会在后续的数据库交互(比如步骤3的读取)时,自动把数据库的最新状态同步到一级缓存中的实体实例上。这个过程是Hibernate的「脏检查」和快照机制在起作用:
- Hibernate会为缓存中的每个实体保存一个初始状态快照;
- 当数据库中的实体发生变化时,Hibernate会对比快照和最新数据库状态,自动更新缓存中的实体实例属性;
- 所以你步骤1拿到的实体对象,和步骤3读取的对象其实是同一个Java实例(可以打印
hashCode()验证),属性自然完全一致,也就看不到你预期的「更新前后差异」了。
3. 如何验证并得到预期的差异结果?
如果想看到更新前后的实体差异,你需要打破同一个持久化上下文的共享,或者强制清空一级缓存:
- 方法一:让步骤2的事务独立:给步骤2的Service方法设置事务传播属性为
REQUIRES_NEW,这样它会强制开启一个全新的事务,更新操作完成后会立即提交到数据库,并且和测试的主事务隔离; - 方法二:清空一级缓存:在步骤3读取实体之前,调用
entityManager.clear()(需要注入EntityManager到测试类中),这样Hibernate会清空当前Session的一级缓存,步骤3读取时就会从数据库重新加载一个新的实体实例,和步骤1的实例就会有属性差异了。
关于悲观锁的补充
你用到的PESSIMISTIC_READ和PESSIMISTIC_WRITE是数据库层面的悲观锁,作用是防止并发场景下的脏写或不可重复读,但它们不会影响Hibernate一级缓存的同步机制——缓存同步是ORM框架的内部行为,和数据库锁属于不同层面的机制。
内容的提问来源于stack exchange,提问作者wrapperapps

