hibernatetransactionalpessimistic-lockingstaleobjectstate

Avoid StaleObjectStateException when deleting entity


I have 2 concurrent threads that at the same time enter a (Spring) transaction service.

Using Hibernate, the service method loads some entities, processes those, finds one and deletes it from the DB. The pseudo code is as following:

@Transactional
public MyEntity getAndDelete(String prop) {
    List<MyEntity> list = (List<MyEntity>)sessionFactory
        .getCurrentSession()
        .createCriteria(MyEntity.class)
        .add( Restrictions.eq("prop", prop) )
        .list();

    // process the list, and find one entity
    MyEntity entity = findEntity(list);
    if (entity != null) {
        sessionFactory.getCurrentSession().delete(entity);
    }
    return entity;
}

In case two threads at the same time pass the same parameter, both will "find" the same entity an both will call delete. One of them will fail throwing an org.hibernate.StaleObjectStateException when the session is close.

I would like that both threads would return the entity, with no exception being thrown. In order to achieve this, I tried to lock (with "select... for update") the entity before deleting it, as following:

@Transactional
public MyEntity getAndDelete(String prop) {
    List<MyEntity> list = (List<MyEntity>)sessionFactory
        .getCurrentSession()
        .createCriteria(MyEntity.class)
        .add( Restrictions.eq("prop", prop) )
        .list();

    // process the list, and find one entity
    MyEntity entity = findEntity(list);
    if (entity != null) {
        // reload the entity with "select ...for update"
        // to ensure the exception is not thrown
        MyEntity locked = (MyEntity)sessionFactory
            .getCurrentSession()
            .load(MyEntity.class, entity.getId(), new LockOptions(LockMode.PESSIMISTIC_WRITE));
        if (locked != null) {
            sessionFactory.getCurrentSession().delete(locked);
        }
    }
    return entity;
}

I use load() instead of get() since according to the hibernate APIs, get would return the entity if already in the session, while load should re-read it.

If two threads enter at the same time the method above, one of them blocks a the locking stage, and when the first thread closes the transaction, the second is awoken throwing an org.hibernate.StaleObjectStateException. Why?

Why does the locked load not just return null? How could I achieve this?


Solution

  • I spent some time investigating this issue and I finally understood what happens.

    The PESSIMISTIC_WRITE lock tries to "lock" the Entity that is already loaded in the session, it does not re-read the object from the DB. Debugging the call, I saw that entity == locked returned true (in Java terms). Both variables were pointing to the same instance.

    To force hibernate to reload the Entity, it must be removed from the session first.

    The following code does the trick:

    @Transactional
    public MyEntity getAndDelete(String prop) {
        List<MyEntity> list = (List<MyEntity>)sessionFactory
            .getCurrentSession()
            .createCriteria(MyEntity.class)
            .add( Restrictions.eq("prop", prop) )
            .list();
    
        // process the list, and find one entity
        MyEntity entity = findEntity(list);
        if (entity != null) {
    
            // Remove the entity from the session.
            sessionFactory.getCurrentSession().evict(entity);
    
            // reload the entity with "select ...for update"
            MyEntity locked = (MyEntity)sessionFactory
                .getCurrentSession()
                .get(MyEntity.class, entity.getId(), new LockOptions(LockMode.PESSIMISTIC_WRITE));
            if (locked != null) {
                sessionFactory.getCurrentSession().delete(locked);
            }
        }
        return entity;
    }
    

    The PESSIMISTIC_WRITE mut be used with get instead of load, because otherwise a org.hibernate.ObjectNotFoundExceptionwould be thrown.