The only approach that is consistent with high concurrency and high scalability is optimistic concurrency control with versioning. Version checking uses version numbers, or timestamps, to detect conflicting updates (and to prevent lost updates). Hibernate provides for three possible approaches to writing application code that uses optimistic concurrency. The use cases we show are in the context of long conversations, but version checking also has the benefit of preventing lost updates in single database transactions.
In an implementation without much help from Hibernate, each interaction with the
database occurs in a new Session
and the developer is responsible
for reloading all persistent instances from the database before manipulating them.
This approach forces the application to carry out its own version checking to ensure
conversation transaction isolation. 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(); Transaction t = session.beginTransaction(); int oldVersion = foo.getVersion(); session.load( foo, foo.getKey() ); // load the current state if ( oldVersion != foo.getVersion() ) throw new StaleObjectStateException(); foo.setProperty("bar"); t.commit(); session.close();
The version
property is mapped using <version>
,
and Hibernate will automatically increment it during flush if the entity is
dirty.
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. In that case, last commit wins will be the default strategy for your long conversations. Keep in mind that this might confuse the users of the application, as they might experience lost updates without error messages or a chance to merge conflicting changes.
Clearly, manual version checking is only feasible in very trivial circumstances
and not practical for most applications. Often not only single instances, but
complete graphs of modified objects have to be checked. Hibernate offers automatic
version checking with either an extended Session
or detached instances
as the design paradigm.
A single Session
instance and its persistent instances are
used for the whole conversation, known as session-per-conversation.
Hibernate checks instance versions at flush time, throwing an exception if concurrent
modification is detected. It's up to the developer to catch and handle this exception
(common options are the opportunity for the user to merge changes or to restart the
business conversation with non-stale data).
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, nor does it have to reload instances in every
database transaction.
// foo is an instance loaded earlier by the old session Transaction t = session.beginTransaction(); // Obtain a new JDBC connection, start transaction foo.setProperty("bar"); session.flush(); // Only for last transaction in conversation t.commit(); // Also return JDBC connection session.close(); // Only for last transaction in conversation
The foo
object still knows which Session
it was
loaded in. Beginning a new database transaction on an old session obtains a new connection
and resumes the session. Committing a database transaction disconnects a session
from the JDBC connection and returns the connection to the pool. After reconnection, to
force a version check on data you aren't updating, you may call Session.lock()
with LockMode.READ
on any objects that might have been updated by another
transaction. You don't need to lock any data that you are updating.
Usually you would set FlushMode.MANUAL
on an extended Session
,
so that only the last database transaction cycle is allowed to actually persist all
modifications made in this conversation. Hence, only this last database transaction
would include the flush()
operation, and then also
close()
the session to end the conversation.
This pattern is problematic if the 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 probably
use this strategy only for a few request/response cycles. You should use a
Session
only for a single conversation, as it will soon also
have stale data.
(Note that earlier Hibernate versions required explicit disconnection and reconnection
of a Session
. These methods are deprecated, as beginning and
ending a transaction has the same effect.)
Also note that you should keep the disconnected Session
close
to the persistence layer. In other words, use an EJB stateful session bean to
hold the Session
in a three-tier environment, and don't transfer
it to the web layer (or even serialize it to a separate tier) to store it in the
HttpSession
.
The extended session pattern, or session-per-conversation, is
more difficult to implement with automatic current session context management.
You need to supply your own implementation of the CurrentSessionContext
for this, see the Hibernate Wiki for examples.
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 reattaches them using Session.update()
,
Session.saveOrUpdate()
, or Session.merge()
.
// foo is an instance loaded by a previous Session foo.setProperty("bar"); session = factory.openSession(); Transaction t = session.beginTransaction(); session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already t.commit(); session.close();
Again, Hibernate will check instance versions during flush, throwing an exception if conflicting updates occurred.
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.
You may disable Hibernate's automatic version increment for particular properties and
collections by setting the optimistic-lock
mapping attribute to
false
. Hibernate will then no longer increment versions if the
property is dirty.
Legacy database schemas are often static and can't be modified. Or, other applications
might also access the same database and don't know how to handle version numbers or
even timestamps. In both cases, versioning can't rely on a particular column in a table.
To force a version check without a version or timestamp property mapping, with a
comparison of the state of all fields in a row, turn on optimistic-lock="all"
in the <class>
mapping. Note that this conceptually only works
if Hibernate can compare the old and new state, i.e. if you use a single long
Session
and not session-per-request-with-detached-objects.
Sometimes concurrent modification can be permitted as long as the changes that have been
made don't overlap. If you set optimistic-lock="dirty"
when mapping the
<class>
, Hibernate will only compare dirty fields during flush.
In both cases, with dedicated version/timestamp columns or with full/dirty field
comparison, Hibernate uses a single UPDATE
statement (with an
appropriate WHERE
clause) per entity to execute the version check
and update the information. If you use transitive persistence to cascade reattachment
to associated entities, Hibernate might execute unnecessary updates. This is usually
not a problem, but on update triggers in the database might be
executed even when no changes have been made to detached instances. You can customize
this behavior by setting select-before-update="true"
in the
<class>
mapping, forcing Hibernate to SELECT
the instance to ensure that changes did actually occur, before updating the row.