Hibernate 6.6 Optimistic Lock Exception in Spring Boot Tests After Upgrade
Problem Statement
After upgrading to Spring Boot 3.4.0 (which includes Hibernate 6.6.x), integration tests using @SpringBootTest
suddenly fail with JPA save operations. Tests that previously worked now throw:
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
This occurs when saving entities where the ID is manually set despite being marked as @GeneratedValue
, such as:
// Test code triggering failure
var musterEntity = MusterMetadatenGenerator.createDefaultMuster();
musterMetadatenRepository.saveAndFlush(musterEntity); // Exception here
The entity's ID is defined with auto-generation:
@MappedSuperclass
public class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(updatable = false, nullable = false)
protected UUID id; // Auto-generated but manually set in test
}
Cause
Hibernate 6.6 (included in Spring Boot 3.4.0) introduced critical behavior changes for ID handling:
- Stricter Optimistic Lock Enforcement: When saving entities with manually set IDs that are also annotated with
@GeneratedValue
, Hibernate now treats them as detached (pre-existing) entities. - New Merge Behavior: If the database contains no matching row for the provided ID, Hibernate throws
StaleObjectStateException
(previously an insert was attempted). - Root Issue: Manually setting IDs for entities with
@GeneratedValue
contradicts Hibernate's entity lifecycle rules. This was tolerated in Hibernate 6.5.x but is now explicitly blocked.
Key Change
GenerationType.AUTO
no longer implies allowAssignedIdentifiers=true
. Manually assigned IDs with @GeneratedValue
trigger the new strict handling.
Solution
Remove manual ID assignment in entity creation logic. Let Hibernate generate IDs as configured.
Step-by-Step Fix
Modify Entity Creation Helper: Ensure ID isn't manually set
javastatic <E extends BaseEntity> E createDefaultValues( @NotNull final E source, @NotNull final LocalDate datumAb, @NotNull final LocalDate datumBis ) { final Instant now = Instant.now(); // Remove manual ID assignment // source.setId(UUID.randomUUID()); // Delete this line source.setDatumAb(datumAb); source.setDatumBis(datumBis); source.setBearbeitetAm(now); source.setErstelltAm(now); return source; }
Verify Entity State: Ensure entities have
null
IDs before saving:java@Test void retrieveAllFahrzeugbewegungenByMuster() { var musterEntity = MusterMetadatenGenerator.createDefaultMuster(); // ID should be null before save assertThat(musterEntity.getId()).isNull(); musterMetadatenRepository.saveAndFlush(musterEntity); // ... }
Why This Works
- Aligns with
@GeneratedValue
contract: The DB/provider generates the ID - New entities start with
id=null
, signaling Hibernate to treat them as transient - Fixes the mismatch where Hibernate 6.6 expects generated IDs to be unassigned
Background: Hibernate 6.6 Changes
The core behavior change is in DefaultMergeEventListener
(added in HHH-1661). Key differences:
if (result == null) {
// Would attempt insert even with manual ID
}
if (result == null) {
LOG.trace("Detached instance not found in database");
if (knownTransient == Boolean.FALSE) {
// New behavior: Throw if entity has
// generated @Id but no DB record exists
throw new StaleObjectStateException(entityName, id);
}
}
Best Practice
Always treat @GeneratedValue
fields as read-only for new entities. Never assign explicit IDs unless using @Id
without generation.
When Assigning IDs is Intentional
If your design requires manual ID assignment:
- Replace
@GeneratedValue
with custom ID handling - Use
@Id
without generation:java@Id @Column(updatable = false, nullable = false) protected UUID id; // No @GeneratedValue
- Explicitly manage ID uniqueness in application logic
Additional Resources
- Hibernate 6.6 Migration Guide
- HHH-1661: Entity merge behavior refinement
- Spring Boot issue tracking Hibernate 6.6 upgrade: #43100