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

使用@Transactional和@Rollback时JPA集成测试实体版本更新失败

问题:带@Version注解的实体在Spring集成测试中版本断言失败

背景信息

  • 直接通过beerRepository和beerController开展集成测试,未使用MockMvc
  • 应用启动时H2数据库预加载3个Beer实体
  • 测试类标注@SpringBootTest,测试方法标注@Transactional和@Rollback,确保测试完成后数据库回滚

测试流程

  1. 通过beerRepository获取第一个Beer的有效UUID
  2. 借助映射器将Beer对象转换为BeerDTO对象
  3. 修改BeerDTO的beerName属性
  4. 调用beerController.updateById方法,传入UUID和修改后的BeerDTO
  5. 断言更新结果,核心断言包括version字段自增1

问题现象

版本断言失败:预期值为原version+1,实际读取到的版本仍为原始值(示例:预期1,实际0)

特殊观察

  • 移除@Rollback和@Transactional注解后,测试通过
  • 将BeerServiceJPA中的save()替换为saveAndFlush()后,测试通过

原因分析

这是JPA事务延迟写入机制与测试方法的@Transactional注解共同作用的结果:

  1. 测试方法的@Transactional会让整个测试流程运行在同一个事务中,Hibernate的一级缓存(Session)全程生效
  2. 在BeerServiceJPA中调用save()时,Hibernate仅会将实体变更记录到一级缓存,不会立即同步至数据库,版本号也不会即时更新
  3. 测试后续调用beerRepository.findById()时,直接从一级缓存读取未刷新的旧实体,导致版本号仍为初始值
  4. 移除@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 21:35:16