lundi 6 octobre 2008

How to deal with Pessimistic Locking

There are severals way to perform pessimistic locking :
  1. Using a direct locking mechanism in database (vendor specific)
  2. Using query hints defined statically in named meta data (vendor specific)
  3. Using a Query api that defines the lock programatically (vendor specific)
  4. Using vendor implementation artifacts (vendor specific)
  5. Using the optimistic lock mechanism of JPA
Database locking mechanism
Database lock mechanism is vendor specific but it is very efficient.
For Oracle database, the lock is performed while doing a 'select for update' on a row of a database table.
See here under to get a sample that explains how to lock an entity:

public MyEntity lock( long id)
{
Query query = entityManager.createNativeQuery("select e.* from myentity e where id=?1 for update");
query.setParameter(1, id);
MyEntity found = null;
try {
found = query.getSingleResult();
} catch(EntityNotFoundException e) {} catch(NoResultException e) {}
if(found != null) entityManager.refresh(found);
return found;
}


The lock is acquired until the calling transaction commits or rollbacks.
If a transaction acquires the lock, all others transactions will be blocked on the query.getSingleResult() until the first one exists its transaction.
This mechanism is database specific and is not portable if you consider changing your database provider.

Query hints
Query hints are vendor directives that may be defined statically in named query metadata (annotations or XML).
They are applied at execution time while executing or flushing the query.
See here under to get a sample :


@NamedQuery(name=“MyEntity.loadAndLock”,
query=“SELECT e FROM MyEntity e WHERE e.id = :id”,
hints={
@QueryHint( name=“toplink.pessimistic-lock”, value=“Lock”),
@QueryHint( name=“openjpa.ReadLockLevel”,value=“write”)
}
)

As you can see the toplink.pessimistic-lock hint is dedicated to the TopLink midleware and could not be used in an other provider like Hibernate.
This source code is not portable to an other environment.

Dynamic query hints
Some time you may want to lock dynamically a query using the Query API.
This is more flexible because the lock could be applied on any object and any query without a static definition.

Query query = em.createQuery( “SELECT e FROM MyEntity e WHERE e.id = :id”);
query.setHint( “toplink.pessimistic-lock”,“Lock”);
query.setHint( “openjpa.ReadLockLevel” “write”);
query.setParameter(“id”, objectId)
query.getResultList();



Vendor artifacts
There is an other way to perform a lock using the specific API of the database mapping midleware. For example with Toplink you can write :


public MyEntity loadAndLock( int id)
{
MyEntity e = entityManager.find( MyEntity.class, id);

UnitOfWork uow = (TopLinkEntityManager)entityManager.getUnitOfWork();
uow.refreshAndLockObject(e, LOCK);
return e;
}


you can write also the following source code :

public MyEntity loadAndLock(int id)
{
Query q = entityManager.createQuery( “SELECT e FROM MyEntity e WHERE e.id = :id”);
q.setParameter( “id”, id);
((ObjectLevelReadQuery)((TopLinkQuery)q.getDatabaseQuery())).acquireLocks();
return q.getSingleResult();
}


As you can see, the code is specific to the supplier of the persistence layer and can not in any way be related to a different environment


Lock using JPA feature
At the moment JPA supports only Optimistic Locking but does not support the pessimistic locking mechanism. But if you are familiar with the lock feature of the entity manager you will realize that it is possible to use the traditional lock method to perform a pessimistic locking.

The following source code do almost the same thing as the 'select for update' in oracle database but is portable whatever the environment :

MyEntity entity = entityManager.find( MyEntity.class, id);
entityManager.lock ( entity, LockModeType.WRITE);
entityManager.flush ();


The use of the lock mechanism at entity involves the use of a field-level version on this entity :

@entity
public class Myentity
{
@Id
private Integer id;

@Version
private int version;

...

public int getVersion () {
return version;
}

public void setVersion (int version) {
this.version = version;
}
}


What's happening while locking ?
The first thread that acquires the lock is not blocked but a second one that trys to access the same entity is blocked on the flush() method.
When the first thread commits or rollbacks, the blocked one receive a javax.persistence.OptimisticLockException. This mechanism ensure that the second thread does not take into account data that would have been affected by the first one.
As a result, the source code must be aware that an exception can be lifted and must implement a mechanism to retry.

Note that when the flush() throw an exception, the current transaction is marked for rollback and can not be used for transactional purposes. In this way each retry must be performed using a new transaction.