3.  Object Locking

3.1. Configuring Default Locking
3.2. Configuring Lock Levels at Runtime
3.3. Object Locking APIs
3.4. Lock Manager
3.5. Rules for Locking Behavior
3.6. Known Issues and Limitations

Controlling how and when objects are locked is an important part of maximizing the performance of your application under load. This section describes OpenJPA's APIs for explicit locking, as well as its rules for implicit locking.

3.1.  Configuring Default Locking

You can control OpenJPA's default transactional read and write lock levels through the openjpa.ReadLockLevel and openjpa.WriteLockLevel configuration properties. Each property accepts a value of none, read, write, optimistic, optimistic-force-increment, pessimistic-read, pessimistic-write, pessimistic-force-increment, or a number corresponding to a lock level defined by the lock manager in use. These properties apply only to non-optimistic transactions; during optimistic transactions, OpenJPA never locks objects by default.

You can control the default amount of time OpenJPA will wait when trying to obtain locks through the openjpa.LockTimeout configuration property. Set this property to the number of milliseconds you are willing to wait for a lock before OpenJPA will throw an exception, or to -1 for no limit. It defaults to -1.

Example 9.3.  Setting Default Lock Levels

<property name="openjpa.ReadLockLevel" value="none"/>
<property name="openjpa.WriteLockLevel" value="write"/>
<property name="openjpa.LockTimeout" value="30000"/>

3.2.  Configuring Lock Levels at Runtime

At runtime, you can override the default lock levels through the FetchPlan interface described above. At the beginning of each datastore transaction, OpenJPA initializes the EntityManager 's fetch plan with the default lock levels and timeouts described in the previous section. By changing the fetch plan's locking properties, you can control how objects loaded at different points in the transaction are locked. You can also use the fetch plan of an individual Query to apply your locking changes only to objects loaded through that Query.

public LockModeType getReadLockMode();
public FetchPlan setReadLockMode(LockModeType mode);
public LockModeType getWriteLockMode();
public FetchPlan setWriteLockMode(LockModeType mode);
long getLockTimeout();
FetchPlan setLockTimeout(long timeout);

Controlling locking through these runtime APIs works even during optimistic transactions. At the end of the transaction, OpenJPA resets the fetch plan's lock levels to none. You cannot lock objects outside of a transaction.

Example 9.4.  Setting Runtime Lock Levels

import org.apache.openjpa.persistence.*;

...

EntityManager em = ...;
em.getTransaction().begin();

// load stock we know we're going to update at write lock mode
Query q = em.createQuery("select s from Stock s where symbol = :s");
q.setParameter("s", symbol);
OpenJPAQuery oq = OpenJPAPersistence.cast(q);
FetchPlan fetch = oq.getFetchPlan();
fetch.setReadLockMode(LockModeType.WRITE);
fetch.setLockTimeout(3000); // 3 seconds
Stock stock = (Stock) q.getSingleResult();

// load an object we don't need locked at none lock mode
fetch = OpenJPAPersistence.cast(em).getFetchPlan();
fetch.setReadLockMode(null);
Market market = em.find(Market.class, marketId);

stock.setPrice(market.calculatePrice(stock));
em.getTransaction().commit();

3.3.  Object Locking APIs

In addition to allowing you to control implicit locking levels, OpenJPA provides explicit APIs to lock objects and to retrieve their current lock level.

public LockModeType OpenJPAEntityManager.getLockMode(Object pc);

Returns the level at which the given object is currently locked.

In addition to the standard EntityManager.lock(Object, LockModeType) method, the OpenJPAEntityManager exposes the following methods to lock objects explicitly:

public void lock(Object pc);
public void lock(Object pc, LockModeType mode, long timeout);
public void lockAll(Object... pcs);
public void lockAll(Object... pcs, LockModeType mode, long timeout);
public void lockAll(Collection pcs);
public void lockAll(Collection pcs, LockModeType mode, long timeout);

Methods that do not take a lock level or timeout parameter default to the current fetch plan. The example below demonstrates these methods in action.

Example 9.5.  Locking APIs

import org.apache.openjpa.persistence.*;

// retrieve the lock level of an object
OpenJPAEntityManager oem = OpenJPAPersistence.cast(em);
Stock stock = ...;
LockModeType level = oem.getLockMode(stock);
if (level == OpenJPAModeType.WRITE) ...

...

oem.setOptimistic(true);
oem.getTransaction().begin();

// override default of not locking during an opt trans to lock stock object
oem.lock(stock, LockModeType.WRITE, 1000);
stock.setPrice(market.calculatePrice(stock));

oem.getTransaction().commit();

3.4.  Lock Manager

OpenJPA delegates the actual work of locking objects to the system's org.apache.openjpa.kernel.LockManager. This plugin is controlled by the openjpa.LockManager configuration property. You can write your own lock manager, or use one of the bundled options:

  • mixed: This is an alias for the org.apache.openjpa.jdbc.kernel.MixedLockManager , which implements the JPA 2.0 specification entity locking behaviors. It combines both the optimistic and pessimistic semantics controlled by lock mode argument in methods define in the EntityManager and Query interfaces or OpenJPA lock level properties.

    The mixed LockManager inherits all the properties available from version and pessimistic LockManagers. For example: VersionCheckOnReadLock and VersionUpdateOnWriteLock properties.

    This is the default openjpa.LockManager setting in OpenJPA.

  • pessimistic: This is an alias for the org.apache.openjpa.jdbc.kernel.PessimisticLockManager , which uses SELECT FOR UPDATE statements (or the database's equivalent) to lock the database rows corresponding to locked objects. This lock manager does not distinguish between read locks and write locks; all locks are write locks.

    The pessimistic LockManager can be configured to additionally perform the version checking and incrementing behavior of the version lock manager described below by setting its VersionCheckOnReadLock and VersionUpdateOnWriteLock properties:

    <property name="openjpa.LockManager" value="pessimistic(VersionCheckOnReadLock=true,VersionUpdateOnWriteLock=true)"/>
    
  • version: This is an alias for the org.apache.openjpa.kernel.VersionLockManager. This lock manager does not perform any exclusive locking, but instead ensures read consistency by verifying that the version of all read-locked instances is unchanged at the end of the transaction. Furthermore, a write lock will force an increment to the version at the end of the transaction, even if the object is not otherwise modified. This ensures read consistency with non-blocking behavior.

  • none: This is an alias for the org.apache.openjpa.kernel.NoneLockManager, which does not perform any locking at all.

Note

In order for the version or mixed lock managers to prevent the dirty read phenomenon, the underlying data store's transaction isolation level must be set to the equivalent of "read committed" or higher.

Example 9.6.  Disabling Locking

<property name="openjpa.LockManager" value="none"/>

3.5.  Rules for Locking Behavior

Advanced persistence concepts like lazy-loading and object uniquing create several locking corner-cases. The rules below outline OpenJPA's implicit locking behavior in these cases.

  1. When an object's state is first read within a transaction, the object is locked at the fetch plan's current read lock level. Future reads of additional lazy state for the object will use the same read lock level, even if the fetch plan's level has changed.

  2. When an object's state is first modified within a transaction, the object is locked at the write lock level in effect when the object was first read, even if the fetch plan's level has changed. If the object was not read previously, the current write lock level is used.

  3. When objects are accessed through a persistent relation field, the related objects are loaded with the fetch plan's current lock levels, not the lock levels of the object owning the field.

  4. Whenever an object is accessed within a transaction, the object is re-locked at the current read lock level. The current read and write lock levels become those that the object "remembers" according to rules one and two above.

  5. If you lock an object explicitly through the APIs demonstrated above, it is re-locked at the specified level. This level also becomes both the read and write level that the object "remembers" according to rules one and two above.

  6. When an object is already locked at a given lock level, re-locking at a lower level has no effect. Locks cannot be downgraded during a transaction.

3.6.  Known Issues and Limitations

Due to performance concerns and database limitations, locking cannot be perfect. You should be aware of the issues outlined in this section, as they may affect your application.

  • Typically, during optimistic transactions OpenJPA does not start an actual database transaction until you flush or the optimistic transaction commits. This allows for very long-lived transactions without consuming database resources. When using the pessimistic lock manager, however, OpenJPA must begin a database transaction whenever you decide to lock an object during an optimistic transaction. This is because the pessimistic lock manager uses database locks, and databases cannot lock rows without a transaction in progress. OpenJPA will log an INFO message to the openjpa.Runtime logging channel when it begins a datastore transaction just to lock an object.

  • In order to maintain reasonable performance levels when loading object state, OpenJPA can only guarantee that an object is locked at the proper lock level after the state has been retrieved from the database. This means that it is technically possible for another transaction to "sneak in" and modify the database record after OpenJPA retrieves the state, but before it locks the object. The only way to positively guarantee that the object is locked and has the most recent state to refresh the object after locking it.

    When using the pessimistic lock manager, the case above can only occur when OpenJPA cannot issue the state-loading SELECT as a locking statement due to database limitations. For example, some databases cannot lock SELECTs that use joins. The pessimistic lock manager will log an INFO message to the openjpa.Runtime logging channel whenever it cannot lock the initial SELECT due to database limitations. By paying attention to these log messages, you can see where you might consider using an object refresh to guarantee that you have the most recent state, or where you might rethink the way you load the state in question to circumvent the database limitations that prevent OpenJPA from issuing a locking SELECT in the first place.

  • When using the pessimistic lock manager and named queries you will see the following WARNING message logged if you do not specify a lockMode on the named query or you explicitly set it to LockModeType.NONE. When using the pessimistic lock manager a LockModeType.NONE will always be promoted to LockModeType.READ.

    WARN   [main] openjpa.MetaData - Encountered a read lock level less than LockModeType.READ when processing the NamedQuery annotation "findEmployeeById" in class "org.apache.openjpa.persistence.lockmgr.LockEmployee". Setting query lock level to LockModeType.READ.
    

    If you are using the pessimistic lock manager and you truly do want to set the lock mode to NONE for a given query, you can use a fetch plan to do so.

    OpenJPAQuery q = em.createNamedQuery("findEmployeeById"); 
    FetchPlan fp = q.getFetchPlan();
    fp.setReadLockMode(LockModeType.NONE);