Sunday, 29 September 2019

Spring jpa: ObjectOptimisticLockingFailureException

While working in concurrent environment, we should make sure that the collisions will not occur while performing operations with database. We can achieve the consistency in data writes/reads using locking.

JPA support two types of locking.
a.   Optimistic Locking
b.   Pessimistic Locking

In this post, I am going to explain about optimistic locking.

To achieve optimistic locking, a table maintains a property @Version annotation. While working with,
a.   Every transaction that wants to work with this record reads the record and keep the value of version property.
b.   Whenever transaction wants to update the record, it validates the value of version property again.
c.    If the value of version property changed by some other transactions, then OptimisticLockException is thrown, else the record is updated and value of the version property gets incremented.

Optimistic locking is suitable for read centric applications, where we perform more reads than writes.

If you are using spring jpa to work with database, then ObjectOptimisticLockingFailureException is thrown by spring in case of optimistic lock conflicts.

Let’s see it with an example.

Employee.java
package com.sample.app.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;

@Entity
@Table(name = "employees")
public class Employee {

 @Id
 @GeneratedValue
 @Column(name = "EMPLOYEE_ID")
 private int id;

 @Column(name = "EMPLOYEE_FIRST_NAME")
 private String firstName;

 @Column(name = "EMPLOYEE_LAST_NAME")
 private String lastName;

 @Version
 private Integer version;
 
 public Employee() {
  
 }

 public Employee(String firstName, String lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
 }

 public int getId() {
  return id;
 }

 public void setId(int id) {
  this.id = id;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public Integer getVersion() {
  return version;
 }

 public void setVersion(Integer version) {
  this.version = version;
 }

 @Override
 public String toString() {
  return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", version=" + version
    + "]";
 }

}

EmployeeRepository.java
package com.sample.app.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.sample.app.entity.Employee;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {

}

App.java
package com.sample.app;

import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import com.sample.app.entity.Employee;
import com.sample.app.repository.EmployeeRepository;
import org.springframework.orm.ObjectOptimisticLockingFailureException;

@SpringBootApplication
public class App {
 @Autowired
 private EmployeeRepository empRepo;

 public static void main(String args[]) {
  SpringApplication.run(App.class, args);
 }

 private static int objectIdToUpdate = -1;

 public void sleepNSeconds(int n) {
  try {
   TimeUnit.SECONDS.sleep(n);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
 }

 Thread t1 = new Thread() {
  public void run() {
   Employee emp = empRepo.findById(objectIdToUpdate).get();
   System.out.println(getName() + " read the object " + emp);

   sleepNSeconds(3);

   System.out.println(getName() + " Updating the employee\n");
   emp.setLastName("Manohar");
   emp = empRepo.save(emp);
   System.out.println(emp);
   System.out.println(getName() + " Updated the employee\n");
  }
 };

 Thread t2 = new Thread() {
  public void run() {
   Employee emp = empRepo.findById(objectIdToUpdate).get();
   System.out.println(getName() + " read the object " + emp);

   sleepNSeconds(5);

   System.out.println(getName() + " Updating the employee\n");
   emp.setLastName("Krishna");
   emp = empRepo.save(emp);
   System.out.println(emp);
   System.out.println(getName() + " Updated the employee\n");
  }
 };

 @Bean
 public CommandLineRunner demo() {
  return (args) -> {
   Employee emp1 = new Employee("Naresh", "Deva");
   Employee emp2 = new Employee("Nani", "Kulkarni");
   Employee emp3 = new Employee("Ram", "Kishore");

   empRepo.save(emp1);
   empRepo.save(emp2);
   Employee empToUpdate = empRepo.save(emp3);
   objectIdToUpdate = empToUpdate.getId();

   empRepo.findAll().forEach(System.out::println);

   t1.setName("Thread-1");
   t2.setName("Thread-2");

   t1.start();
   t2.start();

   t1.join();
   t2.join();

  };
 }

}

application.properties
logging.level.root=WARN
logging.level.org.hibernate=ERROR

## H2 specific properties
spring.h2.console.enabled=true
spring.h2.console.path=/h2

spring.datasource.url=jdbc:h2:file:~/db/myOrg.db;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;

spring.datasource.username=krishna
spring.datasource.password=password123

spring.datasource.driverClassName=org.h2.Driver

## JPA specific properties
# Creates the schema, destroying previous data.
spring.jpa.hibernate.ddl-auto=create

spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=false

## Database connection pooling properties
# Number of ms to wait before throwing an exception if no connection is available.
spring.datasource.max-wait=10000

# Maximum number of active connections that can be allocated from this pool at the same time.
spring.datasource.tomcat.max-active=10
spring.datasource.tomcat.max-idle=5
spring.datasource.tomcat.min-idle=3

Total project structure looks like below.


Run App.java, you can see below messages in console.
Employee [id=1, firstName=Naresh, lastName=Deva, version=0]
Employee [id=2, firstName=Nani, lastName=Kulkarni, version=0]
Employee [id=3, firstName=Ram, lastName=Kishore, version=0]
Thread-1 read the object Employee [id=3, firstName=Ram, lastName=Kishore, version=0]
Thread-2 read the object Employee [id=3, firstName=Ram, lastName=Kishore, version=0]
Thread-1 Updating the employee

Employee [id=3, firstName=Ram, lastName=Manohar, version=1]
Thread-1 Updated the employee

Thread-2 Updating the employee

Exception in thread "Thread-2" org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.sample.app.entity.Employee] with identifier [3]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sample.app.entity.Employee#3]
 at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:335)
 at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:253)
 at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
 at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
 at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
 at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:138)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
 at com.sun.proxy.$Proxy68.save(Unknown Source)
 at com.sample.app.App$2.run(App.java:58)
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sample.app.entity.Employee#3]
 at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:321)
 at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:170)
 at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:69)
 at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:901)
 at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:887)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:308)
 at com.sun.proxy.$Proxy66.merge(Unknown Source)
 at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:510)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:359)
 at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:200)
 at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:644)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:608)
 at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.lambda$invoke$3(RepositoryFactorySupport.java:595)
 at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:595)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:295)
 at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
 ... 10 more

You can download complete working application from below this link.
    


Previous                                                    Next                                                    Home

No comments:

Post a Comment