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

如何在测试中触发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;

}

请问我哪里出错了?该如何编写测试来触发这个异常?


问题原因与解决方案

核心原因

  1. 测试类的全局事务:测试类加了@Transactional,整个测试方法在同一个事务中运行。Hibernate的一级缓存会让request1和request2指向同一个对象实例,两次更新只会修改同一个对象的状态,不会触发版本冲突。
  2. 事务传播机制:服务类的@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 13:31:05