Spring Boot 3.4.0でのテスト失敗:Hibernate変更とID生成問題の解決
問題要約
Spring Bootを3.3.6から3.4.0へ更新後、@SpringBootTest
を使用した統合テストでHibernate/JPA操作が突然失敗するようになりました。典型的なエラーは以下の通りです:
org.springframework.orm.ObjectOptimisticLockingFailureException:
Row was updated or deleted by another transaction:
[de.muster.jpa.entity.MusterMetadaten#40add996-a1f9-43f7-a157-cf5692e5ea65]
この問題が発生する主な箇所:
@Test
void retrieveAllFahrzeugbewegungenByMuster() throws Exception {
var musterEntity = MusterMetadatenGenerator.createDefaultMuster();
var musterEntitySaved = musterMetadatenRepository.saveAndFlush(musterEntity); // ここで失敗
...
}
エンティティ生成処理と基本クラス:
static <E extends BaseEntity> E createDefaultMuster() {
...
source.setId(UUID.randomUUID()); // 明示的なID設定
...
}
@MappedSuperclass
public class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // ID自動生成
protected UUID id;
}
根本原因
Spring Boot 3.4.0に同梱されるHibernateが6.5.xから6.6.xにアップグレードされたことで、ID管理の動作が変更されたことが原因です。
Hibernate 6.6での主要変更点
@GeneratedValue
のエンティティに手動でIDを設定するとデータベースエラーが発生- 明示的にIDが設定されたエンティティは「既存エンティティ(detached entity)」と見なされる
- データベースに対応レコードがない場合、
StaleObjectStateException
がスローされる
// Hibernate 6.5(Spring Boot 3.3):
if (result == null) {
// 新しいエンティティとしてINSERTを試行
}
// Hibernate 6.6(Spring Boot 3.4):
if (result == null) {
// 明確にdetached entityと判定されレコードがない場合
// → StaleObjectStateExceptionがスローされる
}
この変更はHHH-1661対応(https://hibernate.atlassian.net/browse/HHH-1661)およびSpring Bootチケット#43100(https://github.com/spring-projects/spring-boot/issues/43100)で導入されました。
解決策
推奨解決方法(ベストプラクティス)
@GeneratedValue
が適用されるフィールドへの明示的なID設定を停止し、Hibernateの自動生成メカニズムを活用:
static <E extends BaseEntity> E createDefaultValues(...) {
final Instant now = Instant.now();
- source.setId(UUID.randomUUID()); // 削除
source.setDatumAb(datumAb);
source.setDatumBis(datumBis);
source.setBearbeitetAm(now);
source.setErstelltAm(now);
return source;
}
重要ポイント
- 正しい動作:
@GeneratedValue
が指定されたフィールドはHibernateが自動管理 - 新しいエンティティではID未設定(null)が必須
- 保存時にHibernateが自動的にUUIDを生成・設定
代替手段(特殊ケース用)
方法1: ID生成戦略明示指定
@MappedSuperclass
public class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID) // 明示指定
protected UUID id;
}
方法2: 旧動作カスタムジェネレータ(非推奨)
特定理由で手動設定が必須の場合はカスタムIDジェネレータを実装:
@IdGeneratorType(LegacySequenceGeneratorImpl.class)
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface LegacySequenceGenerator {
String sequenceName() default "";
// ...その他のパラメータ
}
public class LegacySequenceGeneratorImpl extends SequenceStyleGenerator {
@Override
public boolean allowAssignedIdentifiers() {
return true; // 旧バージョンの挙動を強制
}
// ...設定実装
}
@Entity
public class MusterMetadaten {
@Id
@LegacySequenceGenerator(sequenceName = "muster_id_seq")
private UUID id;
}
注意事項
カスタムジェネレータは非推奨のワークアラウンドです。本来のJPA仕様に反するため、可能であれば生成管理をHibernateに委譲してください。
現象詳細解説
なぜ例外が発生するのか
- 新しいエンティティのIDを手動設定 →
UUID.randomUUID()
- Hibernate 6.6は「IDが設定=永続化済みエンティティ」と解釈
- データベースに存在しないため「他のトランザクションで削除された」判断
OptimisticLockException
がスロー
バージョン間差異の本質
影響範囲
@GeneratedValue
を持つIDフィールド+手動ID設定@Version
アノテーションの非プリミティブ型フィールド- テスト環境だけでなく本番コードにも影響
最終推奨事項
最適な対応:
@GeneratedValue
のフィールドではIDを明示的に設定しない- エンティティファクトリ/ビルダーからID設定処理を除去
- テストデータ生成前の初期状態を適切に管理
Hibernate 6.6のこの変更は、ID管理の曖昧さを排除しオプティミスティックロック動作を厳密化する正当な変更です。アプリケーションの修正によりデータ整合性層の信頼性向上が期待できます。