0

I'm trying to test optimistic locking using JPA's @Version annotation that I've added to my entity object:

@Version    
@Setter(AccessLevel.NONE)
@Column(name = "VERSION")
private long version; 

When I run 2 servers concurrently, I receive a StaleObjectStateException:

Exception message is : Object of class [com.myPackage.WorkQueue] with identifier [9074]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.myPackage.WorkQueue#9074]

I was expecting to see 1) an OptimisticLockException to occur and 2) the @transaction to get rolled back as a result.

The use case is as follows: Entries get inserted into an Oracle Database Table with a status of 'NEW'. Once a thread retrieves the row with the status = 'NEW', it updates the row's status on the Table to 'IN_PROGRESS'. I need to ensure any transactions reading the same row at the same time fail/rollback if another transaction has successfully updated that row.

The Service:

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor=Exception.class, readOnly=false)
public WorkQueue retrieveWorkQueueItemByStatus(WorkQueueStatusEnum workQueueStatus) {
    return workQueueRepository.retrieveWorkQueueItemByStatus(workQueueStatus);
}

The Implementation:

@Override
public WorkQueue retrieveWorkQueueItemByStatus(WorkQueueStatusEnum workQueueStatus) {
    log.debug("Start - Attempting to select a " + workQueueStatus + " workQueue item in retrieveWorkQueueItemByStatus()");  

    try {
        String sql = "SELECT a FROM WorkQueue a WHERE workQueueStatus = :workQueueStatus ORDER BY idWorkQueue ASC";
        TypedQuery<WorkQueue> query = em.createQuery(sql, WorkQueue.class).setParameter("workQueueStatus", workQueueStatus)
        .setFirstResult(0).setMaxResults(1);
        WorkQueue workQueue = (WorkQueue) query.getSingleResult();
        if (workQueue != null) {
            workQueue.setWorkQueueStatus(WorkQueueStatusEnum.IN_PROGRESS);
            WorkQueue updatedWorkQueue = em.merge(workQueue);               
            log.debug("Finish - selected the following workQueue item "+ workQueue.getIdWorkQueue() + " with the Audit Event Key from retrieveWorkQueueItemByStatus() : " + updatedWorkQueue.getAuditEventKey());
            return updatedWorkQueue;
        }
    } catch (IllegalArgumentException iae) {
        log.error("An IllegalArgumentException occured in workQueueRepositoryImpl.retrieveWorkQueueItemByStatus() attempting to execute query : " + sql + ". Exception message is : " + iae.getMessage());
    } catch(Exception ex) {
        log.error("An Exception occured in workQueueRepositoryImpl.retrieveWorkQueueItemByStatus() executing query : " + sql + ". Exception message is : " + ex.getMessage());
    }
    log.debug("Finish - returning null from retrieveWorkQueueItemByStatus()");
    return null;
}
Orby
  • 428
  • 1
  • 9
  • 24

1 Answers1

0

See this question about the exception type.

The transaction is not rolled back automatically. This kind of exception should never be caught and continued using the Hibernate session. The reason is that the session could have been been partially synchronized to the database, therefor the database state could be inconsistent. So let the exception be thrown outside of the transaction, where it is rolled back, and start over with a new session.

Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193
  • are you saying the above syntax is hibernate specific and not JPA specific, therefore, i don't get the right type of exception? Why wouldn't the transaction get rolled back automatically? – Orby Apr 24 '18 at 13:06
  • Stefan, I've removed the try catch statements from the above implementation and added a `throws RuntimeException` to the `retrieveWorkQueueItemByStatus()` method signature. Am I correct assuming the transaction will roll back when a `StaleObjectStateException` gets thrown by Hibernate? – Orby Apr 25 '18 at 09:30
  • I would check the Hibernate code when it is important to you that the transaction is rolled back at the point in time when the exception is thrown. Usually the exception is thrown out of some try (with resources) block and the transaction and session are closed. That's the point in time when it is rolled back at least. – Stefan Steinegger Apr 26 '18 at 11:38