我正在尝试在 Spring 中为 Beer 实体的 updateById 操作实现集成测试,该实体具有用 @Version 注释的版本属性。
背景:
测试流程:
问题: 版本断言测试失败。期望值为 1,但实际值为 0。
观察结果:
有人可以解释为什么会发生这种情况以及我该如何解决它吗?我相信 @Transactional 和 @Rollback 在这里有意义,因为理想情况下更新应该在测试后回滚。
这是相关代码(为简洁起见,进行了简化):
实体/啤酒
@Getter
@Setter
@ToString
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Beer {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@Column(length = 36, columnDefinition = "varchar", updatable = false, nullable = false)
private UUID id;
@Version
private Integer version;
private String beerName;
private BeerStyle beerStyle;
private String upc;
private Integer quantityOnHand;
private BigDecimal price;
private LocalDateTime createdDate;
private LocalDateTime updateDate;
}
存储库/BeerRepository
public interface BeerRepository extends JpaRepository<Beer, UUID> {
}
服务/啤酒服务
public interface BeerService {
void updateBeerById(UUID beerId, BeerDTO beer);
}
服务/BeerServiceJPA
@Service
@Primary
@RequiredArgsConstructor
public class BeerServiceJPA implements BeerService {
private final BeerRepository beerRepository;
private final BeerMapper beerMapper;
@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());
beerRepository.save(foundBeer);
});
}
}
控制器/啤酒控制器
@Slf4j
@RequiredArgsConstructor
@RestController
public class BeerController {
public static final String BEER_PATH = "/api/v1/beer";
public static final String BEER_PATH_ID = BEER_PATH + "/{beerId}";
private final BeerService beerService;
@PutMapping(BEER_PATH_ID)
public ResponseEntity updateById(@PathVariable("beerId")UUID beerId, @RequestBody BeerDTO beer){
beerService.updateBeerById(beerId, beer);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
测试/控制器/BeerControllerIT
@SpringBootTest
class BeerControllerIT {
@Autowired
BeerController beerController;
@Autowired
BeerRepository beerRepository;
@Autowired
BeerMapper beerMapper;
@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));
Beer updatedBeer = beerRepository.findById(beer.getId()).get();
System.out.println(updatedBeer);
assertThat(updatedBeer.getBeerName()).isEqualTo(beerName);
assertThat(updatedBeer.getVersion()).isEqualTo(version+1);
}
}
发生这种情况是因为仅当实体写入数据库时,
@Version
字段的值才会更新。实体写入数据库的时刻由刷新策略决定。默认情况下,使用 FlushMode.AUTO
,其中 Hibernate 尝试将实体尽可能长时间地保留在持久化上下文中,并将其卸载到数据库,仅在事务结束时,或在 JPQL、HQL 或 SQL 查询之前,或者除非使用 flush()
命令手动将持久性上下文卸载到数据库。
对于您的情况,请致电
updateById()
ResponseEntity responseEntity = beerController.updateById(beer.getId(), beerDTO);
不会立即执行 SQL UPDATE 命令,这会推迟到事务结束。
并打电话给
findById()
Beer updatedBeer = beerRepository.findById(beer.getId()).get();
不执行 SELECT 命令,因为该实体已经在持久化上下文中,您可以从中获取除
@Version
之外的所有字段的更新值。
要解决此问题,您可以在读取实体的更新版本之前手动刷新持久化上下文:
entityManager.flush();
或者使用 JPQL、HQL 或 SQL 查询阅读新版本:
TypedQuery<Beer> typedQuery = entityManager.createQuery("SELECT b FROM Beer b WHERE b.id = :id", Beer.class);
typedQuery.setParameter("id", beer.getId());
Beer entity = typedQuery.getSingleResult();