集成测试中JPA用旧值查询返回已修改实体的异常问题
问题原因及解决办法
核心原因
你的问题源于事务共用与JPA一级缓存的交互:
- 测试方法标注了
@Transactional,导致整个测试流程(包括mockMvc触发的过滤器代码)运行在同一个数据库事务中。 - JPA的一级缓存(EntityManager缓存)会在事务生命周期内保留已加载的实体引用。你修改
user的uniqueName并调用saveAndFlush后,缓存中的实体已更新为newUniqueName,且变更被刷到数据库,但事务未提交。 - 过滤器中执行
findUserByUniqueName(oldUniqueName)时,JPA会先执行SQL查询,但由于当前事务内数据库记录已被更新为newUniqueName,SQL本应返回空。但EntityManager缓存中存在该ID的实体,JPA会直接返回缓存中已修改的实体,导致你看到“用旧uniqueName查到新值实体”的矛盾结果。
解决办法
1. 让过滤器使用独立事务
修改过滤器的@Transactional注解,设置传播行为为REQUIRES_NEW,强制开启新事务,使其看不到测试事务中未提交的变更:
@Override @Transactional(propagation = Propagation.REQUIRES_NEW) protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 原有逻辑不变 }
注意:这种方式下,测试事务需要先提交变更,过滤器才能看到数据库中的新值。如果测试需要保留事务回滚,需结合手动事务管理。
2. 测试中手动管理事务,提交变更后再调用接口
去掉测试方法的@Transactional,手动控制事务提交,确保变更生效后再执行接口调用:
@Autowired private PlatformTransactionManager transactionManager; @Test void test() { // 开启事务执行修改操作 TransactionStatus updateStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { User user = userRepository.findUserByUniqueName("oldUniqueName").orElse(null); assertThat(user).isNotNull(); user.setUniqueName("newUniqueName"); userRepository.saveAndFlush(user); // 提交事务,让变更写入数据库 transactionManager.commit(updateStatus); } catch (Exception e) { transactionManager.rollback(updateStatus); throw e; } // 开启新事务执行测试验证(避免污染测试数据) TransactionStatus testStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { // 构造请求头 HttpHeaders headers = new HttpHeaders(); headers.add("UniqueName", "oldUniqueName"); mockMvc .perform(get(endpointUrl).headers(headers)) .andExpect(status().isUnauthorized()); } finally { transactionManager.rollback(testStatus); } }
3. 清除EntityManager缓存(保留测试事务)
如果要保留测试方法的@Transactional,可在saveAndFlush后清除一级缓存,强制后续查询从数据库获取最新数据:
@Autowired private EntityManager entityManager; @Test @Transactional void test() { User user = userRepository.findUserByUniqueName("oldUniqueName").orElse(null); assertThat(user).isNotNull(); user.setUniqueName("newUniqueName"); userRepository.saveAndFlush(user); // 清除EntityManager缓存,强制后续查询走数据库 entityManager.clear(); // 构造请求头 HttpHeaders headers = new HttpHeaders(); headers.add("UniqueName", "oldUniqueName"); mockMvc .perform(get(endpointUrl).headers(headers)) .andExpect(status().isUnauthorized()); }
内容的提问来源于stack exchange,提问作者MehdiB
相关产品推荐
相关产品推荐

