This tutorial shows how to enable Optimistic locking in Spring Data JPA.
Optimistic Locking is a mechanism which ensures that data has not changed externally within a transaction.
To enable Optimistic Locking we need to use a version field annotated with @Version. This annotation is provided by JPA specification (tutorial).
Example
Entity
@Entity
public class Employee{
private @Id
@GeneratedValue
Long id;
private String name;
private String dept;
private int salary;
@Version
private long version;
.............
}
Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
Example client
This example shows that version number increases with each update:
@Component
public class ExampleClient {
@Autowired
private EmployeeRepository repo;
public void run() {
Employee employee = Employee.create("Diana", "Admin", 3000);
repo.save(employee);
Long employeeId = employee.getId();
System.out.println("-- employee persisted --");
System.out.println(findEmployee(employeeId));
System.out.println(" -- updating salary to 2000 --");
Employee employee2 = findEmployee(employeeId);
employee2.setSalary(2000);
repo.save(employee2);
System.out.println(findEmployee(employeeId));
System.out.println(" -- updating salary to 4000 --");
Employee employee3 = findEmployee(employeeId);
employee3.setSalary(4000);
repo.save(employee3);
System.out.println(findEmployee(employeeId));
}
private Employee findEmployee(long employeeId) {
Optional<Employee> opt = repo.findById(employeeId);
return opt.isPresent()? opt.get(): null;
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
ExampleClient exampleClient = context.getBean(ExampleClient.class);
exampleClient.run();
EntityManagerFactory emf = context.getBean(EntityManagerFactory.class);
emf.close();
}
} -- employee persisted -- Employee{id=1, name='Diana', dept='Admin', salary=3000, version=0} -- updating salary to 2000 -- Employee{id=1, name='Diana', dept='Admin', salary=2000, version=1} -- updating salary to 4000 -- Employee{id=1, name='Diana', dept='Admin', salary=4000, version=2}
Following example uses two threads to simulate two users. The two users going to do simultaneous updates to the same entity (entity having same id). Both users load the entity with same version number. During the first user's transaction, the second user completes his transaction hence increases the version number by one, which leads to OptimisticLockingException for the first user when he attempts to commit his transaction.
@Component
public class ExampleClient2 {
@Autowired
private EmployeeRepository repo;
public void run(ApplicationContext context) {
Employee employee = Employee.create("Diana", "Admin", 3000);
repo.save(employee);
Long employeeId = employee.getId();
System.out.println("-- employee persisted --");
System.out.println(findEmployee(employeeId));
ExecutorService es = Executors.newFixedThreadPool(2);
//user 1
es.execute(() -> {
System.out.println(" -- user1 updating salary to 2000 --");
Employee employee2 = findEmployee(employeeId);
System.out.println("user1 loaded entity: " + employee2);
employee2.setSalary(2000);
//little delay
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
repo.save(employee2);
} catch (Exception e) {
System.err.println("user1 " + e);
System.out.println("user1 after error: " + findEmployee(employeeId));
return;
}
System.out.println("user1 finished: " + findEmployee(employeeId));
});
//user 2
es.execute(() -> {
System.out.println(" -- user2 updating salary to 4000 --");
Employee employee3 = findEmployee(employeeId);
System.out.println("user2 loaded entity: " + employee3);
employee3.setSalary(4000);
try {
repo.save(employee3);
} catch (Exception e) {
System.err.println("user2: " + e);
System.out.println("user2 after error: " + findEmployee(employeeId));
return;
}
System.out.println("user2 finished: " + findEmployee(employeeId));
});
es.shutdown();
try {
es.awaitTermination(10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
EntityManagerFactory emf = context.getBean(EntityManagerFactory.class);
emf.close();
}
private Employee findEmployee(long employeeId) {
Optional<Employee> opt = repo.findById(employeeId);
return opt.isPresent() ? opt.get() : null;
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
ExampleClient2 exampleClient = context.getBean(ExampleClient2.class);
exampleClient.run(context);
}
}
-- employee persisted --
Employee{id=1, name='Diana', dept='Admin', salary=3000, version=0}
-- user1 updating salary to 2000 --
-- user2 updating salary to 4000 --
user1 loaded entity: Employee{id=1, name='Diana', dept='Admin', salary=3000, version=0}
user2 loaded entity: Employee{id=1, name='Diana', dept='Admin', salary=3000, version=0}
user2 finished: Employee{id=1, name='Diana', dept='Admin', salary=4000, version=1}
user1 javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.logicbig.example.Employee#1]
user1 after error: Employee{id=1, name='Diana', dept='Admin', salary=4000, version=1}
Example ProjectDependencies and Technologies Used: - spring-data-jpa 2.1.2.RELEASE: Spring Data module for JPA repositories.
Uses org.springframework:spring-context version 5.1.2.RELEASE - hibernate-core 5.3.5.Final: Hibernate's core ORM functionality.
Implements javax.persistence:javax.persistence-api version 2.2 - h2 1.4.197: H2 Database Engine.
- JDK 1.8
- Maven 3.5.4
|