Thursday 3 March 2011

Pessimistic and Optimistic Locking in JPA 2.0

A most welcome addition to JPA 2.0 was the introduction of pessimistic locking. This allows the entity manager (or query) to lock a database record thereby preventing other transactions from changing the same record. This will ensure data consistency but at a performance cost so for each entity you need to ask yourself what is the chance of contention?

Let's look at the two approaches in more detail:

Optimistic Locking

Optimistic locking is the preferred approach when modifying entities that are infrequently updated. Optimistic locking can be explicit as will be shown later or implicit by using a version attribute. A version attribute is an attribute which has been annotated with @Version. This attribute will then get incremented when the transaction commits. The below class shows an example of its usage:

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.PersistenceContext;
import javax.persistence.Version;
 

@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    private static final int ID_LENGTH=36;

    @Id
    @Column(length = ID_LENGTH)
    private String id;
    @Version
    private int versionNumber;

    // etc ...

}
 
There are two optimistic lock modes:

  • OPTIMISTIC or READ - locks the entity when the transaction reads it for entities wit a version
  • OPTIMISTIC_FORCE_INCREMENT or WRITE - locks the entity when the transaction reads it for entities with a version and increments the version attribute

Pessimistic Locking

Pessimistic locking can be applied to all entities regardless of whether they have a version attribute or not.
 
There are three pessimistic lock modes:

  • PESSIMISTIC_READ - locks the entity when the transaction reads it and allows reads from other transactions
  • PESSIMISTIC_WRITE - locks the entity when the transaction updates it but does not allow reads, updates or deletes from other transactions
  • PESSIMISTIC_FORCE_INCREMENT - locks the entity when the transaction reads it and increments the version attribute (if present)

There are many different ways to implement a pessimistic or optimistic lock using the Entity Manager and Query interfaces as shown in the example classes below:

import java.io.Serializable;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;


@Named("customerManager")
@ApplicationScoped
public class CustomerManager implements Serializable {


    private static final long serialVersionUID = 1L;

    @PersistenceContext
    private EntityManager entityManager;


    public void setName(String id, String name) {

        Customer customer = 

            entityManager.find(Customer.class, id);
        entityManager.lock(customer,

            LockModeType.OPTIMISTIC_FORCE_INCREMENT);
        customer.setName(name);
    }


    public Customer findCustomer(String id) {

        return entityManager.find(Customer.class, id, 

            LockModeType.OPTIMISTIC);
    }


    public Customer 

        applyBankCharges(String id, Double charges) {
 
        Customer customer = 

            entityManager.find(Customer.class, id);
        customer.setBalance(customer.getBalance() - charges);
        if (customer.getBalance() < 0.0) {
            // overwrite changes and return the refreshed 

            // and locked object
            entityManager.refresh(customer, 

                LockModeType.PESSIMISTIC_WRITE);
        }
        return customer;
    }


    public List<Customer> 

        findCustomerByPartialName(String partialName) {
 
        Query query = entityManager.createQuery(
                "SELECT c FROM Customer c WHERE c.name" + 

                " LIKE :partialName").
                setParameter("partialName", partialName);
        query.setLockMode( 

            LockModeType.PESSIMISTIC_FORCE_INCREMENT);
        return query.getResultList();
    }
}



import javax.persistence.Entity;
import javax.persistence.LockModeType;
import javax.persistence.NamedQuery;


@Entity

@NamedQuery(name = "findByNameQuery",
    query = "SELECT c FROM Customer c WHERE c.name LIKE :name",
    lockMode = LockModeType.PESSIMISTIC_FORCE_INCREMENT)
public class Customer extends BaseEntity {


    private String name;

    private Double balance;

    public void setName(String name) {

        this.name = name;
    }
    public Double getBalance() {

        return this.balance;
    }
    public void setBalance(Double balance) {

        this.balance = balance;
    }
}

 

Finally, a word on exceptions. If a PessimisticLockException or an OptimisticLockException are thrown then the transaction is rolled back as they are both RuntimeExceptions.