Skip to content

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:

java
// Test code triggering failure
var musterEntity = MusterMetadatenGenerator.createDefaultMuster();
musterMetadatenRepository.saveAndFlush(musterEntity); // Exception here

The entity's ID is defined with auto-generation:

java
@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:

  1. 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.
  2. New Merge Behavior: If the database contains no matching row for the provided ID, Hibernate throws StaleObjectStateException (previously an insert was attempted).
  3. 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

  1. Modify Entity Creation Helper: Ensure ID isn't manually set

    java
    static <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;
    }
  2. 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:

<CodeGroup> <CodeGroupItem title="Hibernate 6.5 (Spring Boot < 3.4.0)">
java
if (result == null) {
   // Would attempt insert even with manual ID
}
</CodeGroupItem> <CodeGroupItem title="Hibernate 6.6 (Spring Boot ≥ 3.4.0" active>
java
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);
   }
}
</CodeGroupItem> </CodeGroup>

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:

  1. Replace @GeneratedValue with custom ID handling
  2. Use @Id without generation:
    java
    @Id
    @Column(updatable = false, nullable = false)
    protected UUID id; // No @GeneratedValue
  3. Explicitly manage ID uniqueness in application logic

Additional Resources