Skip to content

Spring Boot 3.4.0集成测试JPA异常解决

问题现象

升级Spring Boot到3.4.0后,使用@SpringBootTest的集成测试出现异常:

java
@Test
void retrieveAllFahrzeugbewegungenByMuster() throws Exception {
    var musterEntity = MusterMetadatenGenerator.createDefaultMuster();
    var musterEntitySaved = musterMetadatenRepository.saveAndFlush(musterEntity); // 此处崩溃
}

异常信息:

log
org.springframework.orm.ObjectOptimisticLockingFailureException: 
Row was updated or deleted by another transaction...
Caused by: org.hibernate.StaleObjectStateException: 
Row was updated or deleted by another transaction...

相关实体类定义:

java
@Data
@MappedSuperclass
public class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected UUID id; // 使用自动生成策略
    // 其他字段...
}

实体创建工具方法:

java
static <E extends BaseEntity> E createDefaultValues(E source) {
    source.setId(UUID.randomUUID()); // ✘ 手动设置ID
    // 设置其他字段...
    return source;
}

核心问题

在创建新实体时手动设置了ID值(UUID.randomUUID()),而该字段使用了@GeneratedValue自动生成策略

原因分析

此问题由Spring Boot 3.4.0中Hibernate升级至6.6.x(之前为6.5.x)引起,具体变动包含:

Hibernate关键行为变更

  1. 旧的Hibernate 6.5.x行为
    当尝试保存一个存在ID但数据库中无对应记录的对象时,日志记录不确定状态但不会立即失败

  2. 新的Hibernate 6.6.x行为

java
// Hibernate 6.6.2.Final 源代码片段
if (result == null) {
    LOG.trace("Detached instance not found in database");
    if (knownTransient == Boolean.FALSE) {
        throw new StaleObjectStateException(entityName, id); // 抛出异常
    }
}

根本原因

  • 手动设置ID + @GeneratedValue注解导致Hibernate将对象识别为**分离态(detached)**实体
  • Hibernate认定该实体应存在于数据库中(因有ID)
  • 当数据库中无匹配记录时,抛出StaleObjectStateException
  • 此变更是为了解决HHH-1661问题:合并已删除的分离实体时防止错误插入新数据

解决方案

推荐修复方式

在创建实体时避免手动设置ID

java
// 修改后的实体创建工具方法
static <E extends BaseEntity> E createDefaultValues(E source) {
    // 移除手动ID设置: source.setId(UUID.randomUUID());
    source.setDatumAb(datumAb);
    source.setDatumBis(datumBis);
    // 其他字段设置...
    return source;
}

完整修复步骤

最佳实践

  1. 定位所有手动设置ID的位置(常见于测试数据工具类)
  2. 移除所有@GeneratedValue字段的手动ID赋值
  3. 仅通过Repository执行保存操作:
    java
    @Test
    void validEntityCreation() {
        BaseEntity entity = new BaseEntity(); // ID为null
        // 设置必要字段...
        BaseEntity saved = repository.save(entity); // ID由Hibernate自动生成
        assertNotNull(saved.getId()); // 保存后ID非空
    }
  4. 需要引用已存在实体时,应从数据库中查询而非新建带ID实体

迁移注意事项

场景正确做法错误做法
创建新实体ID保持为null手动设置UUID
更新已有实体通过Repository获取完整实体修改手动构造实体并设置ID
测试用例数据准备使用未初始化ID的测试对象工厂硬编码ID值

补充提示

  • 版本兼容性:此变更仅影响从Spring Boot 3.3.x升至3.4.x的项目
  • 测试策略:建议在升级前后运行集成测试比较JPA行为差异
  • 备用方案:极端情况下若需保留旧行为(不推荐),可考虑覆盖Hibernate配置实现,但会失去框架升级带来的优化

通过移除手动ID赋值,Hibernate能正确识别新实体并生成ID,从而解决ObjectOptimisticLockingFailureException异常问题。

重要提醒

此解决方案仅适用于新实体创建。如需操作已存在实体:

java
// 正确方式:从数据库获取已存在实体
Optional<Entity> exist = repository.findById(id);
if(exist.isPresent()) {
    Entity entity = exist.get();
    // ... 修改操作
}