The most important point about Hibernate and concurrency control is that it is very easy to understand. Hibernate directly uses JDBC connections and JTA resources without adding any additional locking behavior. We highly recommend you spend some time with the JDBC, ANSI, and transaction isolation specification of your database management system.
Hibernate does not lock objects in memory. Your application can expect the behavior as
defined by the isolation level of your database transactions. Note that thanks to the
Session
, which is also a transaction-scoped cache, Hibernate
provides repeatable reads for lookup by identifier and entity queries (not
reporting queries that return scalar values).
In addition to versioning for automatic optimistic concurrency control, Hibernate also
offers a (minor) API for pessimistic locking of rows, using the
SELECT FOR UPDATE
syntax. Optimistic concurrency control and
this API are discussed later in this chapter.
We start the discussion of concurrency control in Hibernate with the granularity of
Configuration
, SessionFactory
, and
Session
, as well as database transactions and long conversations.
A SessionFactory
is an expensive-to-create, threadsafe object
intended to be shared by all application threads. It is created once, usually on
application startup, from a Configuration
instance.
A Session
is an inexpensive, non-threadsafe object that should be
used once, for a single request, a conversation, single unit of work, and then discarded.
A Session
will not obtain a JDBC Connection
(or a Datasource
) unless it is needed, hence consume no
resources until used.
To complete this picture you also have to think about database transactions. A database transaction has to be as short as possible, to reduce lock contention in the database. Long database transactions will prevent your application from scaling to highly concurrent load. Hence, it is almost never good design to hold a database transaction open during user think time, until the unit of work is complete.
What is the scope of a unit of work? Can a single Hibernate Session
span several database transactions or is this a one-to-one relationship of scopes? When
should you open and close a Session
and how do you demarcate the
database transaction boundaries?
First, don't use the session-per-operation antipattern, that is,
don't open and close a Session
for every simple database call in
a single thread! Of course, the same is true for database transactions. Database calls
in an application are made using a planned sequence, they are grouped into atomic
units of work. (Note that this also means that auto-commit after every single
SQL statement is useless in an application, this mode is intended for ad-hoc SQL
console work. Hibernate disables, or expects the application server to do so,
auto-commit mode immediately.) Database transactions are never optional, all
communication with a database has to occur inside a transaction, no matter if
you read or write data. As explained, auto-commit behavior for reading data
should be avoided, as many small transactions are unlikely to perform better than
one clearly defined unit of work. The latter is also much more maintainable
and extensible.
The most common pattern in a multi-user client/server application is
session-per-request. In this model, a request from the client
is sent to the server (where the Hibernate persistence layer runs), a new Hibernate
Session
is opened, and all database operations are executed in this unit
of work. Once the work has been completed (and the response for the client has been prepared),
the session is flushed and closed. You would also use a single database transaction to
serve the clients request, starting and committing it when you open and close the
Session
. The relationship between the two is one-to-one and this
model is a perfect fit for many applications.
The challenge lies in the implementation. Hibernate provides built-in management of
the "current session" to simplify this pattern. All you have to do is start a
transaction when a server request has to be processed, and end the transaction
before the response is sent to the client. You can do this in any way you
like, common solutions are ServletFilter
, AOP interceptor with a
pointcut on the service methods, or a proxy/interception container. An EJB container
is a standardized way to implement cross-cutting aspects such as transaction
demarcation on EJB session beans, declaratively with CMT. If you decide to
use programmatic transaction demarcation, prefer the Hibernate Transaction
API shown later in this chapter, for ease of use and code portability.
Your application code can access a "current session" to process the request
by simply calling sessionFactory.getCurrentSession()
anywhere
and as often as needed. You will always get a Session
scoped
to the current database transaction. This has to be configured for either
resource-local or JTA environments, see Section 2.5, “Contextual Sessions”.
Sometimes it is convenient to extend the scope of a Session
and
database transaction until the "view has been rendered". This is especially useful
in servlet applications that utilize a separate rendering phase after the request
has been processed. Extending the database transaction until view rendering is
complete is easy to do if you implement your own interceptor. However, it is not
easily doable if you rely on EJBs with container-managed transactions, as a
transaction will be completed when an EJB method returns, before rendering of any
view can start. See the Hibernate website and forum for tips and examples around
this Open Session in View pattern.
The session-per-request pattern is not the only useful concept you can use to design units of work. 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. Consider the following example:
The first screen of a dialog opens, the data seen by the user has been loaded in
a particular Session
and database transaction. The user is free to
modify the objects.
The user clicks "Save" after 5 minutes and expects his modifications to be made persistent; he also expects that he was the only person editing this information and that no conflicting modification can occur.
We call this unit of work, from the point of view of the user, a long running conversation (or application transaction). There are many ways how you can implement this in your application.
A first naive implementation might keep the Session
and database
transaction open during user think time, with locks held in the database to prevent
concurrent modification, and to guarantee isolation and atomicity. This is of course
an anti-pattern, since lock contention would not allow the application to scale with
the number of concurrent users.
Clearly, we have to use several database transactions to implement the conversation. In this case, maintaining isolation of business processes becomes the partial responsibility of the application tier. A single conversation 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 (e.g. in a wizard-style dialog spanning several request/response cycles). This is easier to implement than it might sound, especially if you use Hibernate's features:
Automatic Versioning - Hibernate can do automatic optimistic concurrency control for you, it can automatically detect if a concurrent modification occurred during user think time. Usually we only check at the end of the conversation.
Detached Objects - If you decide to use the already discussed session-per-request pattern, all loaded instances will be in detached state during user think time. Hibernate allows you to reattach the objects and persist the modifications, the pattern is called session-per-request-with-detached-objects. Automatic versioning is used to isolate concurrent modifications.
Extended (or Long) Session - The Hibernate
Session
may be disconnected from the underlying JDBC
connection after the database transaction has been committed, and reconnected
when a new client request occurs. This pattern is known as
session-per-conversation and makes
even reattachment unnecessary. Automatic versioning is used to isolate
concurrent modifications and the Session
is usually
not allowed to be flushed automatically, but explicitly.
Both session-per-request-with-detached-objects and session-per-conversation have advantages and disadvantages, we discuss them later in this chapter in the context of optimistic concurrency control.
An application may concurrently access the same persistent state in two
different Session
s. However, an instance of a persistent class
is never shared between two Session
instances. Hence there are
two different notions of identity:
foo.getId().equals( bar.getId() )
foo==bar
Then for objects attached to a particular Session
(i.e. in the scope of a Session
) the two notions are equivalent, and
JVM identity for database identity is guaranteed by Hibernate. 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). Conflicts are
resolved using (automatic versioning) at flush/commit time, using an optimistic approach.
This approach leaves Hibernate and the database to worry about concurrency; it also provides
the best scalability, since guaranteeing identity in single-threaded units of work only doesn't
need expensive locking or other means of synchronization. The application never needs to
synchronize on any business object, as long as it sticks to a single thread per
Session
. Within a Session
the application may safely use
==
to compare objects.
However, an application that uses ==
outside of a Session
,
might see unexpected results. This might occur even in some unexpected places, for example,
if you put two detached instances into the same Set
. Both might have the same
database identity (i.e. they represent the same row), but JVM identity is by definition not
guaranteed for instances in detached state. The developer has to override the equals()
and hashCode()
methods in persistent classes and implement
his own notion of object equality. There is one caveat: Never use the database
identifier to implement equality, use a business key, a combination of unique, usually
immutable, attributes. The database identifier will change if a transient object is made
persistent. If the transient instance (usually together with detached instances) is held in a
Set
, changing the hashcode breaks the contract of the Set
.
Attributes for business keys don't have to be as stable as database primary keys, you only
have to guarantee stability as long as the objects are in the same Set
. See
the Hibernate website for a more thorough discussion of this issue. Also note that this is not
a Hibernate issue, but simply how Java object identity and equality has to be implemented.
Never use the anti-patterns session-per-user-session or session-per-application (of course, there are rare exceptions to this rule). Note that some of the following issues might also appear with the recommended patterns, make sure you understand the implications before making a design decision:
A Session
is not thread-safe. Things which are supposed to work
concurrently, like HTTP requests, session beans, or Swing workers, will cause race
conditions if a Session
instance would be shared. If you keep your
Hibernate Session
in your HttpSession
(discussed
later), you should consider synchronizing access to your Http session. Otherwise,
a user that clicks reload fast enough may use the same Session
in
two concurrently running threads.
An exception thrown by Hibernate means you have to rollback your database transaction
and close the Session
immediately (discussed later in more detail).
If your Session
is bound to the application, you have to stop
the application. Rolling back the database transaction doesn't put your business
objects back into the state they were at the start of the transaction. This means the
database state and the business objects do get out of sync. Usually this is not a
problem, because exceptions are not recoverable and you have to start over after
rollback anyway.
The Session
caches every object that is in persistent state (watched
and checked for dirty state by Hibernate). This means it grows endlessly until you
get an OutOfMemoryException, if you keep it open for a long time or simply load too
much data. One solution for this is to call clear()
and evict()
to manage the Session
cache, but you most likely should consider a
Stored Procedure if you need mass data operations. Some solutions are shown in
Chapter 13, Batch processing. Keeping a Session
open for the duration
of a user session also means a high probability of stale data.