如何在测试中触发ObjectOptimisticLockingFailureException?现有测试未生效
问题:无法触发乐观锁异常的测试排查
我在两个不同事务中保存对象时遇到如下乐观锁异常:
org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [] with identifier []: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : []
但编写测试尝试触发该异常时,所有测试均通过,未触发异常。相关代码如下:
测试代码
@Slf4j @Rollback @Transactional @DataJpaTest @ActiveProfiles({"test", "h2"}) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Import(value = { StatusService.class }) class StatusServiceTest { @Autowired private RequestRepository repository; @Autowired private StatusService statusService; private Request request; private final UUID requestUID = UUID.randomUUID(); @BeforeEach void setUp() { request = new Request(); request.setRequestUID(requestUID); repository.save(request); } @Test void updateStatus() { Request request1 = repository.findByRequestUID(requestUID); Request request2 = repository.findByRequestUID(requestUID); statusService.updateStatus(request1, "Done"); statusService.updateStatus(request2, "In_progress"); } }
服务类代码
@Service @Slf4j @RequiredArgsConstructor public class StatusService { private final RequestRepository repository; @Transactional public void updateStatus(Request request, String status) { // 简单业务逻辑 request.setStatus(status); repository.save(request); // 报错行 } }
实体类代码
@Entity @Getter @Setter @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Request { @Id @GeneratedValue(strategy = GenerationType.AUTO) @JsonIgnore protected Long id; protected UUID requestUID; private String status; // 其他字段 @JsonIgnore @Version private Integer version; }
请问我哪里出错了?该如何编写测试来触发这个异常?
问题原因与解决方案
核心原因
- 测试类的全局事务:测试类加了
@Transactional,整个测试方法在同一个事务中运行。Hibernate的一级缓存会让request1和request2指向同一个对象实例,两次更新只会修改同一个对象的状态,不会触发版本冲突。 - 事务传播机制:服务类的
@Transactional默认是PROPAGATION_REQUIRED,会复用测试类的全局事务,两次更新都在同一个事务中执行,版本号不会被持久化到数据库,Hibernate无法检测到乐观锁冲突。
修复方案
1. 调整服务类的事务传播级别
给服务类的updateStatus方法设置事务传播级别为REQUIRES_NEW,强制开启新事务,确保每次更新都在独立事务中执行:
@Service @Slf4j @RequiredArgsConstructor public class StatusService { private final RequestRepository repository; @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateStatus(Request request, String status) { request.setStatus(status); repository.save(request); repository.flush(); // 手动刷新,确保版本号立即写入数据库 } }
2. 修改测试类,移除全局事务并清除缓存
去掉测试类的@Transactional注解,同时在第一次更新后清除Hibernate一级缓存,确保第二次加载的是数据库最新版本的对象:
@Slf4j @Rollback @DataJpaTest @ActiveProfiles({"test", "h2"}) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Import(value = { StatusService.class }) class StatusServiceTest { @Autowired private RequestRepository repository; @Autowired private StatusService statusService; @Autowired private EntityManager entityManager; private final UUID requestUID = UUID.randomUUID(); @BeforeEach void setUp() { Request request = new Request(); request.setRequestUID(requestUID); repository.saveAndFlush(request); } @Test void shouldThrowOptimisticLockException() { // 第一次加载对象并更新 Request request1 = repository.findByRequestUID(requestUID); statusService.updateStatus(request1, "Done"); // 清除一级缓存,确保第二次加载的是数据库最新版本 entityManager.clear(); // 第二次加载对象并更新,触发乐观锁异常 Request request2 = repository.findByRequestUID(requestUID); Assertions.assertThrows(ObjectOptimisticLockingFailureException.class, () -> { statusService.updateStatus(request2, "In_progress"); }); } }
关键要点
- 必须让两次更新处于独立的事务中,才能让版本号被持久化到数据库
- 需要清除Hibernate的一级缓存,或者重新从数据库加载对象,确保两次操作的是持有不同版本号的对象实例
- 手动调用
repository.flush()可以确保事务提交前版本号被写入数据库,避免延迟更新导致的冲突检测失效
内容的提问来源于stack exchange,提问作者Grigorii Kostarev
相关产品推荐
相关产品推荐

