Sebastiaan van den Broek
Sebastiaan van den Broek

Reputation: 6331

Can I temporarily disable optimistic locking in Hibernate even if the entity has a @Version property?

I have an issue where I get a StaleStateException because a certain delete of an entity is executed in 2 threads. This is because a thread started by a REST call is deleting the object and calls another service to clean some things up there, but this other service puts some data on a Kafka stream as a result. This Kafka stream is constantly being read from another thread and also results in the object being deleted. But depending who gets there first I can get this exception:

Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: HikariProxyPreparedStatement@821401258 wrapping delete from notification_setting where id='f47ef4b1-eb54-4d3a-a6f0-fc4518704288'::uuid and db_version=0

Now in this particular case with the delete being done by a system action, I really don't care about the StateStateException and resulting OptimisticLockingException. Both actions just have to delete the object and I don't mind who gets to it first. But I don't want to remove the @Version property entirely from this entity. Can I temporarily disable the optimistic locking for a particular delete?

The code isn't too relevant but here it is. I already tried deleting by id but a deleteAll has the same issue.

eventLevelSubscriptions.forEach(
            eventLevelSubscription -> notificationEventLevelSubscriptionRepo.deleteById(eventLevelSubscription.getId()));
notificationSettings.forEach(notificationSetting -> notificationSettingRepo.deleteById(notificationSetting.getId()));
em.flush();

for (NotificationSettingKey notificationSettingKey : notificationSettingKeys) {
    kafkaWritingService.removeNotificationSetting(producer, notificationSettingKey);
}

Note the flush because we need to make sure this part succeeds before updating another stream on Kafka.

I tried simply try/catching the whole code because I don't even need to perform the deletes in the second thread as they're already done in the first thread, but then Hibernate seems to get in a corrupt state where an exception still is thrown in the end.

Exception in thread "Thread-6" org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:753)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:631)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
...
at java.base/java.lang.Thread.run(Unknown Source) (note: this is my kafka consumer thread)

Upvotes: 2

Views: 2377

Answers (1)

mentallurg
mentallurg

Reputation: 5207

A) You cannot disable optimistic locking. Why are you using it at all? This exception may be an indicator that your logic is inconsistent. Check if your logic is really correct.

B) If after reviewing you still want to keep the logic as it is, it means you should treat this exception in such cases as a correct behavior (otherwise back to A and review/change the logic), which means you should catch the exception.

C) If your current code is running within a single transaction: Don't delete all entities within a single step, because some of them might have been deleted, the others have not. A solution can be following:

  • Put call of deleteById() to a new method
  • Set transaction propagation to REQUIRES_NEW for this method
  • For transaction propagation to have effect put this method to a separate class
  • In the forEach() call this method instead of calling the repository

D) Consider also a non object oriented approach: Use JPQL or SQL to delete entries. Advantage: There will be no exception if some entries have already been deleted earlier.

Upvotes: 2

Related Questions