升级Spring Boot 4.1后事务场景下JPA Exists查询返回错误结果
Spring Boot 4.1 升级后事务内EXISTS查询返回错误结果问题
问题表现
升级到Spring Boot 4.1后,部分带@Transactional注解的测试用例执行失败,失败用例代码如下:
@SpringBootTest @Transactional class SpringTestApplicationTests { @Autowired RoleRepository roleRepository; @Autowired UserRepository userRepository; Role role; @BeforeEach public void setUp() { role = new Role(); role.name = "Admin"; role = roleRepository.saveAndFlush(role); } // 第二个断言执行失败 @Test void test_exists() { assertThat(roleRepository.isRoleReferencedByAnyUser(role.id)).isEqualTo(false); var user = new User(); user.name = "admin"; user.role = role; userRepository.saveAndFlush(user); assertThat(roleRepository.isRoleReferencedByAnyUser(role.id)).isEqualTo(true); } }
对应Repository层查询代码:
public interface RoleRepository extends JpaRepository<Role, Long> { @Query(value = "select exists (select 1 from User u where u.role.id = :id)") boolean isRoleReferencedByAnyUser(long id); }
失败位置为保存用户后的第二次断言,预期返回true实际返回false。调试过程中确认的现象:
- 移除测试方法上的
@Transactional注解,测试可正常通过 - 去掉第一次调用
isRoleReferencedByAnyUser的前置校验逻辑,仅保留保存用户后的查询断言,测试可正常通过,正常运行的用例代码如下:
// 可正常执行 @Test public void test_single() { var user = new User(); user.name = "admin"; user.role = role; userRepository.saveAndFlush(user); assertThat(roleRepository.isRoleReferencedByAnyUserCount(role.id)).isEqualTo(true); }
- 将EXISTS查询改写为
count()聚合判断逻辑,测试可正常通过,改写后的查询语句如下:
@Query(value = "select (count(u) > 0) from User u where u.role.id = :id") boolean isRoleReferencedByAnyUser(long id);
- 降级回Spring Boot 4版本,测试可正常通过
- 直接在H2控制台执行相同EXISTS语句,可返回正确结果,排除数据库本身逻辑问题。
根因说明
该问题不是业务代码使用不当,是Spring Boot 4.1默认依赖的Hibernate 6.6.x版本存在的一级缓存Bug:
同一事务内第一次执行select exists形式的投影查询时,Hibernate会错误将查询结果存入持久化上下文(一级缓存),后续相同参数的同语句查询不会实际发送SQL到数据库,直接返回第一次缓存的结果,即使中间已经插入了关联数据,也会拿到第一次查询得到的false值,导致断言失败。
所有排查到的现象都可以对应这个根因:
- 移除
@Transactional后,两次查询分别在独立事务中执行,一级缓存不共享,不会命中错误缓存 - 去掉第一次前置查询,没有提前缓存错误结果,查询直接走数据库拿到正确值
count()聚合查询不会触发该错误缓存逻辑,因此改写后可正常返回- Spring Boot 4依赖的Hibernate版本更低,不存在这个Bug,因此降级后正常。
修复方式
- 临时规避:将EXISTS查询改写为
count(u) > 0的聚合写法,避开触发Bug的逻辑 - 临时规避:在第二次执行查询前调用
EntityManager.clear()清空一级缓存,强制查询访问数据库 - 彻底修复:将Hibernate版本升级到6.6.1.Final及以上,官方已经在该版本修复了EXISTS投影查询结果被错误缓存的问题。
内容的提问来源于stack exchange,提问作者Tim Niederhäuser
相关产品推荐
相关产品推荐

