You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:15:34