In last tutorial we saw how to use @Version annotation to enable Optimistic locking. Following example produces a situation which throws OptimisticLockException . This exception is thrown if an optimistic locking conflict occurs, typically on flush or at commit time. In that case, the current transaction is marked for rollback.
Example
The Entity
@Entity
public class Employee {
@Id
@GeneratedValue
private Integer id;
@Version
private long version;
private String name;
private String department;
.............
}
Producing OptimisticLockException
We are going to update the same entity simultaneously in two threads. We will put first transaction to sleep (via Thread.sleep()) to make sure that a version conflict occurs.
public class OptimisticLockExceptionExample {
private static EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("example-unit");
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
try {
persistEmployee();
es.execute(() -> {
try {
updateEmployee1();
} catch (Exception e) {
System.out.println("-- exception thrown during update 1 --");
e.printStackTrace();
}
});
es.execute(() -> {
try {
updateEmployee2();
} catch (Exception e) {
System.out.println("-- exception thrown during update 2 --");
e.printStackTrace();
}
});
es.shutdown();
//wait for the threads to finish
try {
es.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
loadEmployee();
} finally {
entityManagerFactory.close();
}
}
private static void updateEmployee1() {
System.out.println("Update 1 starts, changing dept to Sales");
EntityManager em = entityManagerFactory.createEntityManager();
Employee employee = em.find(Employee.class, 1);
em.getTransaction().begin();
System.out.println("Lock Mode for update 1: " + em.getLockMode(employee));
employee.setDepartment("Sales");
try {
System.out.println("Pausing first transaction for 1 second");
//wait for 1 sec before commit
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("committing first transaction");
em.getTransaction().commit();
em.close();
System.out.println("Employee updated 1: " + employee);
}
private static void updateEmployee2() {
System.out.println("Update 2 starts, changing dept to Admin");
EntityManager em = entityManagerFactory.createEntityManager();
Employee employee = em.find(Employee.class, 1);
em.getTransaction().begin();
System.out.println("Lock Mode for update 2: " + em.getLockMode(employee));
employee.setDepartment("Admin");
em.getTransaction().commit();
em.close();
System.out.println("Employee updated 2: " + employee);
}
private static void loadEmployee() {
EntityManager em = entityManagerFactory.createEntityManager();
Employee employee = em.find(Employee.class, 1);
System.out.println("Employee loaded: " + employee);
}
public static void persistEmployee() {
Employee employee = new Employee("Joe", "IT");
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
em.persist(employee);
em.getTransaction().commit();
em.close();
System.out.println("Employee persisted: " + employee);
}
}
Output
Employee persisted: Employee{id=1, version=0, name='Joe', department='IT'}
Update 1 starts, changing dept to Sales
Update 2 starts, changing dept to Admin
Lock Mode for update 2: OPTIMISTIC
Lock Mode for update 1: OPTIMISTIC
Pausing first transaction for 1 second
Employee updated 2: Employee{id=1, version=1, name='Joe', department='Admin'}
committing first transaction
-- exception thrown during update 1 --
javax.persistence.RollbackException: Error while committing the transaction
at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:77)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:71)
at com.logicbig.example.OptimisticLockExceptionExample.updateEmployee1(OptimisticLockExceptionExample.java:66)
at com.logicbig.example.OptimisticLockExceptionExample.lambda$main$0(OptimisticLockExceptionExample.java:22)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: javax.persistence.OptimisticLockException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.hibernate.internal.ExceptionConverterImpl.wrapStaleStateException(ExceptionConverterImpl.java:214)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:88)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:157)
at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:61)
... 6 more
Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:67)
at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:54)
at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:46)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3198)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3077)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3457)
at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:145)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:589)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:463)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:337)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1437)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:493)
at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3207)
at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2413)
at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:473)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:156)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$100(JdbcResourceLocalTransactionCoordinatorImpl.java:38)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:231)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:68)
... 5 more
Employee loaded: Employee{id=1, version=1, name='Joe', department='Admin'}
Note that, version number did not increase for the first update (where the exception occurred).
Above example purposely creates a situation which throws OptimisticLockException exception, but even in a carefully designed concurrent application there's no way to avoid it because simultaneous updates can occur any time by multiple users (sitting in different threads or JVMs). We just need to handle this exception in a user friendly manner to inform about the conflict so that user can attempt to update the data again.
Example ProjectDependencies and Technologies Used: - h2 1.4.196: H2 Database Engine.
- hibernate-core 5.2.12.Final: The core O/RM functionality as provided by Hibernate.
Implements javax.persistence:javax.persistence-api version 2.1 - JDK 1.8
- Maven 3.3.9
|