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.
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.
No comments:
Post a Comment