使用@Transactional和@Rollback时JPA集成测试实体版本更新失败
问题:带@Version注解的实体在Spring集成测试中版本断言失败
背景信息
- 直接通过
beerRepository和beerController开展集成测试,未使用MockMvc - 应用启动时H2数据库预加载3个Beer实体
- 测试类标注
@SpringBootTest,测试方法标注@Transactional和@Rollback,确保测试完成后数据库回滚
测试流程
- 通过
beerRepository获取第一个Beer的有效UUID - 借助映射器将Beer对象转换为BeerDTO对象
- 修改BeerDTO的
beerName属性 - 调用
beerController.updateById方法,传入UUID和修改后的BeerDTO - 断言更新结果,核心断言包括version字段自增1
问题现象
版本断言失败:预期值为原version+1,实际读取到的版本仍为原始值(示例:预期1,实际0)
特殊观察
- 移除
@Rollback和@Transactional注解后,测试通过 - 将
BeerServiceJPA中的save()替换为saveAndFlush()后,测试通过
原因分析
这是JPA事务延迟写入机制与测试方法的@Transactional注解共同作用的结果:
- 测试方法的
@Transactional会让整个测试流程运行在同一个事务中,Hibernate的一级缓存(Session)全程生效 - 在
BeerServiceJPA中调用save()时,Hibernate仅会将实体变更记录到一级缓存,不会立即同步至数据库,版本号也不会即时更新 - 测试后续调用
beerRepository.findById()时,直接从一级缓存读取未刷新的旧实体,导致版本号仍为初始值 - 移除
@Transactional时,每个操作都是独立事务,save()后事务提交会自动刷新缓存,版本号正常更新;使用saveAndFlush()则会强制将缓存变更同步到数据库,立即更新版本号
修复方案
方案1:测试中手动刷新缓存(推荐)
调用更新接口后,手动刷新beerRepository的缓存,确保读取到数据库中的最新实体:
@Rollback @Transactional @Test void updateExistingBeer() { Beer beer = beerRepository.findAll().get(0); Integer version = beer.getVersion(); BeerDTO beerDTO = beerMapper.beerToBeerDto(beer); beerDTO.setId(null); beerDTO.setVersion(null); final String beerName = "UPDATED"; beerDTO.setBeerName(beerName); ResponseEntity responseEntity = beerController.updateById(beer.getId(), beerDTO); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(204)); // 手动刷新缓存,强制从数据库读取最新数据 beerRepository.flush(); Beer updatedBeer = beerRepository.findById(beer.getId()).get(); assertThat(updatedBeer.getBeerName()).isEqualTo(beerName); assertThat(updatedBeer.getVersion()).isEqualTo(version+1); }
方案2:修改Service层的保存逻辑
将BeerServiceJPA中的save()替换为saveAndFlush(),强制立即同步数据库:
@Override public void updateBeerById(UUID beerId, BeerDTO beer) { beerRepository.findById(beerId).ifPresent(foundBeer -> { foundBeer.setBeerName(beer.getBeerName()); foundBeer.setBeerStyle(beer.getBeerStyle()); foundBeer.setUpc(beer.getUpc()); foundBeer.setPrice(beer.getPrice()); // 使用saveAndFlush替代save,强制同步数据库 beerRepository.saveAndFlush(foundBeer); }); }
方案3:通过EntityManager刷新实体
注入EntityManager,手动刷新目标实体或清空缓存:
@Autowired EntityManager entityManager; // 在测试的更新操作后调用 entityManager.flush(); entityManager.clear(); // 也可仅刷新特定实体:entityManager.refresh(updatedBeer);
内容的提问来源于stack exchange,提问作者Zanarkand
相关产品推荐
相关产品推荐

