Facing with ObjectOptimisticLocking FailureException after migrating to Hibernate 6.6.2.Final

Hi! After migration to spring-boot 3.4.0 and hibernate 6.6.2.Final we faced with error

org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect):[SolvencyEntity#186500]

I have two entities with one-to-one relationship. In the child entity I have a field that is reference to parent entity

    @Id
    @Column(name = "PARTY_ID")
    private Long partyId;

    @JoinColumn(name = "PARTY_ID")
    @OneToOne
    @MapsId
    private PartyEntity party;

And vice versa

 @OneToOne(mappedBy = "party", cascade = CascadeType.ALL)
    private SolvencyEntity solvency;

When I try to create child entity like this I get error

Please share the full entity mappings so we can have a better understanding of what is happening (e.g. are the entities versioned?). Also, we don’t know what the getCurrentParty() method does, and what state the PartyEntity returned by it is in - the error message is pretty self-explanatory, might be that one of your entities is transient, thus triggering the exception during merge (which is what Spring does when calling save on a repository AFAIK).

Thanks for reply!
Only PartyEntity is versioned and getCurrentParty returns entity that was saved in previous cucumber step and solvencyEntity is just created and wanted to be saved.
solvencyEntity is transient but it is annotated as cascade = CascadeType.ALL in PartyEntity so why error is thrown?

Once again, with this little context and without seeing the full mappings and logic of your application it’s very hard to give you an answer. Please try reproducing this error using Hibernate only, you can start from our test case templates, and if you think this is a bug please open a new report in our issue tracker.

Hi, I’m having the same issue after upgrading to Spring Boot 3.4.0 which brings in Hibernate 6.6.x

The Kotlin code is quite simple…

I have a CrudRepository:

@Repository
@Transactional
interface UploaderRepository: CrudRepository<Uploader, String> {
    fun findByEmailAddress(email: String): Uploader?
}

Uploader entity:

@Entity
data class Uploader (
    @Id
    @UuidGenerator
    val uuid: String = "",

    @field:Email
    val emailAddress: String
)

and a service:

@Service
class UploadService(private val uploaderRepository: UploaderRepository) {

    fun getOrCreateUploader(emailAddress: String): Uploader =
        uploaderRepository.findByEmailAddress(emailAddress)
            ?: uploaderRepository.save(Uploader(emailAddress = emailAddress))
}

But when getOrCreateUploader is executed with an email that is not already associated with an Uploader, then uploaderRepository.save(Uploader(emailAddress = emailAddress)) throws this exception:

org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bright.externaluploader.model.Uploader#]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:325) ~[spring-orm-6.2.0.jar:6.2.0]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244) ~[spring-orm-6.2.0.jar:6.2.0]
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:560) ~[spring-orm-6.2.0.jar:6.2.0]
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-6.2.0.jar:6.2.0]
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:343) ~[spring-tx-6.2.0.jar:6.2.0]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:160) ~[spring-tx-6.2.0.jar:6.2.0]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:165) ~[spring-data-jpa-3.4.0.jar:3.4.0]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
	at org.springframework.data.repository.core.support.MethodInvocationValidator.invoke(MethodInvocationValidator.java:96) ~[spring-data-commons-3.4.0.jar:3.4.0]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.2.0.jar:6.2.0]
	at jdk.proxy2/jdk.proxy2.$Proxy150.save(Unknown Source) ~[na:na]
	at [...]UploadService.getOrCreateUploader(UploadService.kt:64) ~[main/:na]

I’ve read the migration guide for Hibernate 6.6 which mentions this:

Previously, merging a detached entity resulted in a SQL insert whenever there was no matching row in the database (for example, if the object had been deleted in another transaction). This behavior was unexpected and violated the rules of optimistic locking.

An OptimisticLockException is now thrown when it is possible to determine that an entity is definitely detached, but there is no matching row. For this determination to be possible, the entity must have either:

a generated @Id field, or

a non-primitive @Version field.

For entities which have neither, it’s impossible to distinguish a new instance from a deleted detached instance, and there is no change from the previous behavior.

But I don’t really understand why it’s throwing the exception for a new instance of the entity. How else are we supposed to insert new values in the database?

Please try setting the field to null instead:

or consider using the UUID type.

Thank you! That worked. Could you please explain why this change is needed? What has changed internally so that empty strings are no longer valid?

I don’t know how Kotlin works, but Hibernate ORM will instantiate a default object instance to determine the “default” value. This is used to differentiate a “new” object from a “detached” during EntityManager#merge.

It was just a guess that this could be the reason for Hibernate ORM to assume the object passed as uploaderRepository.save(Uploader(emailAddress = emailAddress)) is detached. Not sure if it’s a bug or an unfortunate side effect of Kotlin, but at least assigning an empty string is fishy.

Thanks for the reply!