Chapter 10. Transactions And Concurrency

Hibernate is not itself a database. It is a lightweight object-relational mapping tool. Transaction management is delegated to the underlying database connection. If the connection is enlisted with JTA, operations performed by the Session are atomically part of the wider JTA transaction. Hibernate can be seen as a thin adapter to JDBC, adding object- oriented semantics.

10.1. Configurations, Sessions and Factories

A SessionFactory is an expensive-to-create, threadsafe object intended to be shared by all application threads. A Session is an inexpensive, non-threadsafe object that should be used once, for a single business process, and then discarded. For example, when using Hibernate in a servlet-based application, servlets could obtain a SessionFactory using

SessionFactory sf = (SessionFactory)getServletContext().getAttribute("my.session.factory");

Each call to a service method could create a new Session, flush() it, commit() its connection, close() it and finally discard it. (The SessionFactory may also be kept in JNDI or in a static Singleton helper variable.)

In a stateless session bean, a similar approach could be used. The bean would obtain a SessionFactory in setSessionContext(). Then each business method would create a Session, flush() it and close() it. Of course, the application should not commit() the connection. (Leave that to JTA, the database connection participates automatically in container-managed transactions.)

We use the Hibernate Transaction API as discussed previously, a single commit() of a Hibernate Transaction flushes the state and commits any underlying database connection (with special handling of JTA transactions).

Ensure you understand the semantics of flush(). Flushing synchronizes the persistent store with in-memory changes but not vice-versa. Note that for all Hibernate JDBC connections/transactions, the transaction isolation level for that connection applies to all operations executed by Hibernate!

The next few sections will discuss alternative approaches that utilize versioning to ensure transaction atomicity. These are considered "advanced" approaches to be used with care.

10.2. Threads and connections

You should observe the following practices when creating Hibernate Sessions:

  • Never create more than one concurrent Session or Transaction instance per database connection.

  • Be extremely careful when creating more than one Session per database per transaction. The Session itself keeps track of updates made to loaded objects, so a different Session might see stale data.

  • The Session is not threadsafe! Never access the same Session in two concurrent threads. A Session is usually only a single unit-of-work!

10.3. Considering object identity

The application may concurrently access the same persistent state in two different units-of-work. However, an instance of a persistent class is never shared between two Session instances. Hence there are two different notions of identity:

Database Identity

foo.getId().equals( bar.getId() )

JVM Identity

foo==bar

Then for objects attached to a particular Session, the two notions are equivalent. However, while the application might concurrently access the "same" (persistent identity) business object in two different sessions, the two instances will actually be "different" (JVM identity).

This approach leaves Hibernate and the database to worry about concurrency. The application never needs to synchronize on any business object, as long as it sticks to a single thread per Session or object identity (within a Session the application may safely use == to compare objects).

10.4. Optimistic concurrency control

Many business processes require a whole series of interactions with the user interleaved with database accesses. In web and enterprise applications it is not acceptable for a database transaction to span a user interaction.

Maintaining isolation of business processes becomes the partial responsibility of the application tier, hence we call this process a long running application transaction. A single application transaction usually spans several database transactions. It will be atomic if only one of these database transactions (the last one) stores the updated data, all others simply read data.

The only approach that is consistent with high concurrency and high scalability is optimistic concurrency control with versioning. Hibernate provides for three possible approaches to writing application code that uses optimistic concurrency.

10.4.1. Long session with automatic versioning

A single Session instance and its persistent instances are used for the whole application transaction.

The Session uses optimistic locking with versioning to ensure that many database transactions appear to the application as a single logical application transaction. The Session is disconnected from any underlying JDBC connection when waiting for user interaction. This approach is the most efficient in terms of database access. The application need not concern itself with version checking or with reattaching detached instances.

// foo is an instance loaded earlier by the Session
session.reconnect();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.disconnect();

The foo object still knows which Session it was loaded it. As soon as the Session has a JDBC connection, we commit the changes to the object.

This pattern is problematic if our Session is too big to be stored during user think time, e.g. an HttpSession should be kept as small as possible. As the Session is also the (mandatory) first-level cache and contains all loaded objects, we can propably use this strategy only for a few request/response cycles. This is indeed recommended, as the Session will soon also have stale data.

10.4.2. Many sessions with automatic versioning

Each interaction with the persistent store occurs in a new Session. However, the same persistent instances are reused for each interaction with the database. The application manipulates the state of detached instances originally loaded in another Session and then "reassociates" them using Session.update() or Session.saveOrUpdate().

// foo is an instance loaded by a previous Session
foo.setProperty("bar");
session = factory.openSession();
session.saveOrUpdate(foo);
session.flush();
session.connection().commit();
session.close();

You may also call lock() instead of update() and use LockMode.READ (performing a version check, bypassing all caches) if you are sure that the object has not been modified.

10.4.3. Application version checking

Each interaction with the database occurs in a new Session that reloads all persistent instances from the database before manipulating them. This approach forces the application to carry out its own version checking to ensure application transaction isolation. (Of course, Hibernate will still update version numbers for you.) This approach is the least efficient in terms of database access. It is the approach most similar to entity EJBs.

// foo is an instance loaded by a previous Session
session = factory.openSession();
int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() );
if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.close();

Of course, if you are operating in a low-data-concurrency environment and don't require version checking, you may use this approach and just skip the version check.

10.5. Session disconnection

The first approach described above is to maintain a single Session for a whole business process thats spans user think time. (For example, a servlet might keep a Session in the user's HttpSession.) For performance reasons you should

  1. commit the Transaction (or JDBC connection) and then

  2. disconnect the Session from the JDBC connection

before waiting for user activity. The method Session.disconnect() will disconnect the session from the JDBC connection and return the connection to the pool (unless you provided the connection).

Session.reconnect() obtains a new connection (or you may supply one) and restarts the session. After reconnection, to force a version check on data you aren't updating, you may call Session.lock() on any objects that might have been updated by another transaction. You don't need to lock any data that you are updating.

Heres an example:

SessionFactory sessions;
List fooList;
Bar bar;
....
Session s = sessions.openSession();

Transaction tx = null;
try {
    tx = s.beginTransaction();

    fooList = s.find(
    	"select foo from eg.Foo foo where foo.Date = current date"
        // uses db2 date function
    );
    bar = (Bar) s.create(Bar.class);

    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    s.close();
    throw e;
}
s.disconnect();

Later on:

s.reconnect();

try {
    tx = s.beginTransaction();

    bar.setFooTable( new HashMap() );
    Iterator iter = fooList.iterator();
    while ( iter.hasNext() ) {
        Foo foo = (Foo) iter.next();
        s.lock(foo, LockMode.READ);    //check that foo isn't stale
        bar.getFooTable().put( foo.getName(), foo );
    }

    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    throw e;
}
finally {
    s.close();
}

You can see from this how the relationship between Transactions and Sessions is many-to-one, A Session represents a conversation between the application and the database. The Transaction breaks that conversation up into atomic units of work at the database level.

10.6. Pessimistic Locking

It is not intended that users spend much time worring about locking strategies. Its usually enough to specify an isolation level for the JDBC connections and then simply let the database do all the work. However, advanced users may sometimes wish to obtain exclusive pessimistic locks, or re-obtain locks at the start of a new transaction.

Hibernate will always use the locking mechanism of the database, never lock objects in memory!

The LockMode class defines the different lock levels that may be acquired by Hibernate. A lock is obtained by the following mechanisms:

  • LockMode.WRITE is acquired automatically when Hibernate updates or inserts a row.

  • LockMode.UPGRADE may be acquired upon explicit user request using SELECT ... FOR UPDATE on databases which support that syntax.

  • LockMode.UPGRADE_NOWAIT may be acquired upon explicit user request using a SELECT ... FOR UPDATE NOWAIT under Oracle.

  • LockMode.READ is acquired automatically when Hibernate reads data under Repeatable Read or Serializable isolation level. May be re-acquired by explicit user request.

  • LockMode.NONE represents the absence of a lock. All objects switch to this lock mode at the end of a Transaction. Objects associated with the session via a call to update() or saveOrUpdate() also start out in this lock mode.

The "explicit user request" is expressed in one of the following ways:

  • A call to Session.load(), specifying a LockMode.

  • A call to Session.lock().

  • A call to Query.setLockMode().

If Session.load() is called with UPGRADE or UPGRADE_NOWAIT, and the requested object was not yet loaded by the session, the object is loaded using SELECT ... FOR UPDATE. If load() is called for an object that is already loaded with a less restrictive lock than the one requested, Hibernate calls lock() for that object.

Session.lock() performs a version number check if the specified lock mode is READ, UPGRADE or UPGRADE_NOWAIT. (In the case of UPGRADE or UPGRADE_NOWAIT, SELECT ... FOR UPDATE is used.)

If the database does not support the requested lock mode, Hibernate will use an appropriate alternate mode (instead of throwing an exception). This ensures that applications will be portable.