JPA - OptimisticLockException

[Updated: Dec 15, 2017, Created: Dec 12, 2017]

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 Project

Dependencies 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.
  • JDK 1.8
  • Maven 3.3.9

OptimisticLockException Example Select All Download
  • jpa-optimistic-lock-exception-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
        • resources
          • META-INF

See Also