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关键行为变更
旧的Hibernate 6.5.x行为
当尝试保存一个存在ID但数据库中无对应记录的对象时,日志记录不确定状态但不会立即失败新的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;
}
完整修复步骤
最佳实践
- 定位所有手动设置ID的位置(常见于测试数据工具类)
- 移除所有
@GeneratedValue
字段的手动ID赋值 - 仅通过Repository执行保存操作:java
@Test void validEntityCreation() { BaseEntity entity = new BaseEntity(); // ID为null // 设置必要字段... BaseEntity saved = repository.save(entity); // ID由Hibernate自动生成 assertNotNull(saved.getId()); // 保存后ID非空 }
- 需要引用已存在实体时,应从数据库中查询而非新建带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();
// ... 修改操作
}