Hibernate Optimistic Locking
The question I would like to address in this post is about Hibernate
optimistic locking, optimistic locking with no special version number or
timestamp. While pessimistic locking is an available option in
Hibernate to address concurrency control, for most enterprise
applications, optimistic locking is the preferred choice. As the
Hibernate documentation states:
“The only
approach that is consistent with high concurrency and high scalability,
is optimistic concurrency control with versioning”
Hibernate can provide optimistic locking via version number or
effectivity timestamp. To learn more about either approach, you can see
an
earlier post of mine, or check out the
Hibernate documentation.
Optimistic locking via version number or effectivity timestamp is
automatically managed by Hibernate. Simply provide the column to hold
the version or timestamp, add the optimistic lock characteristics to
your Hibernate mapping (via XML or annotations), and Hibernate does the
rest.
Optimistic Locking sans Version or Timestamp
However, there may be situations whereby adding a version number or
timestamp column to your database table is not possible. This might be
the case when a legacy database is in place and many applications use
the table and cannot or will not be updated to use a new
version/timestamp column for optimistic locking. In this case,
Hibernate can compare the column values of a table row against the state
of the persistent object to check that nothing in a row was modified
before updating the row. There are two forms of this state-checking
optimistic locking: all-state-check or dirty-state-check.
Optimistic-lock = ‘all’
When you set the optimistic-lock attribute on the <class>
element in the Hibernate mapping file to the value ‘all’ (as shown on
the Vehicle class mapping below), you are informing Hibernate to check
that no column in the associated row has changed since the persistent
object was retrieved and before updating the row in the database.
1: <hibernate-mapping package="com.intertech.domain">
2: <class name="Vehicle" dynamic-update="true" optimistic-lock="all">
3: <id name="id">
4: <generator class="increment" />
5: </id>
6: <property name="make" />
7: <property name="model" />
8: <property name="vin" />
9: </class>
10: </hibernate-mapping>
For example, say you used Hibernate to obtain an instance of Vehicle and then updated it’s make as shown by the code below.
1: SessionFactory sf = new Configuration().configure()
2: .buildSessionFactory();
3: Session sess = sf.openSession();
4: Transaction trx = sess.beginTransaction();
5: Vehicle v = (Vehicle) sess.get(Vehicle.class, 1L);
6: v.setMake("ford");
7: sess.saveOrUpdate(v);
8: trx.commit();
9: sess.close();
Hibernate would actually use
all
the existing persistent object values (the last known state values) to
form the SQL where-clause used in the update call. Note that every
column is mentioned in the where-clause of the example, even the old
value of the changed property (make in this case). If any other
concurrent activity on this row changed the data of the row (or deleted
the row), Hibernate would detect this by the fact that the where-clause
would fail to match the row (resulting in a StaleObjectStateException.
1: Hibernate:
2: update
3: Vehicle
4: set
5: make='Kia'
6: where
7: id=1
8: and make='Ford'
9: and model='SUV'
10: and vin=12345
Optimistic-lock = ‘dirty’
As an alternative, you can also allow Hibernate to only use modified
or dirty properties in the where-clause of the update. When mapping the
persistent class, set the optimistic-lock attribute to a value of
‘dirty’ (as shown below).
1: <hibernate-mapping package="com.intertech.domain">
2: <class name="Vehicle" dynamic-update="true" optimistic-lock="dirty">
3: <id name="id">
4: <generator class="increment" />
5: </id>
6: <property name="make" />
7: <property name="model" />
8: <property name="vin" />
9: </class>
10: </hibernate-mapping>
Hibernate now uses only the last known state values of modified
persistent object properties to form the SQL where-clause used in the
update call. As an example, assume this code is used to update a
Vehicle’s make and model state.
1: SessionFactory sf = new Configuration().configure()
2: .buildSessionFactory();
3: Session sess = sf.openSession();
4: Transaction trx = sess.beginTransaction();
5: Vehicle v = (Vehicle) sess.get(Vehicle.class, 1L);
6: v.setMake("Chevy");
7: v.setModel("sedan");
8: sess.saveOrUpdate(v);
9: trx.commit();
10: sess.close();
The update SQL only includes checks of the existing row’s make and model columns, that is the dirty properties.
1: Hibernate:
2: update
3: Vehicle
4: set
5: make='Chevy',
6: model='sedan'
7: where
8: id=1
9: and make='Kia'
10: and model='SUV'
This alternative allows other concurrent processes to update the same
row so long as they do not modify the same columns. For example,
another application could be updating the vin number of our vehicle at
the same time we are updating the make and model and neither of the
applications suffer a concurrency error. The merits of allowing this
type of simultaneous update are questionable (at best). From a business
perspective, only you can determine if modifications that do not
conflict constitute change that should cause a concurrency failure or
not. Hibernate provides the options, you must determine whether the
technology fits with your definition of protecting against concurrent
change issues.
Dynamic-update must be True
It should be noted that in both of these non-version/timestamp
optimistic locking options, you must enable dynamic-update (set the
dynamic-update=’true’ attribute) in the class mapping as shown above.
This is because Hibernate cannot generate the SQL for these update
statements at application startup (it wouldn’t know what to include in
the update statement where-clauses until a change is actually made).
Big Limitations of Non-Version/Timestamp Optimistic Locking
Regardless of whether you use the optimistic-lock=all or
optimistic-lock=dirty strategy, there are some big limitations to
Hibernate’s optimistic locking that does not use version numbers or
timestamps.
No Detached
When you close a session, Hibernate loses its persistence context and
ability to track what changes on the objects. Therefore, you cannot
use the optimistic-lock=all or optimistic-lock=dirty option unless you
retrieve and modify the persistent object in the same session (i.e.
persistence context). If you attempt to get a persistent object in one
session, detach the object and then update the object in another
session, you will actually create a situation of potentially
catastrophic last-in-wins updates. For example, try this code with
optimistic-lock set to ‘all’ on the Vehicle class mapping.
1: SessionFactory sf = new Configuration().configure()
2: .buildSessionFactory();
3: Session sess = sf.openSession();
4: Transaction trx = sess.beginTransaction();
5: Vehicle v = (Vehicle) sess.get(Vehicle.class, 1L);
6: trx.commit();
7: sess.close();
8: System.out.println("vehicle now detached: " + v);
9:
10: v.setVin(7890);
11:
12: //reattach and update
13: sess = sf.openSession();
14: trx = sess.beginTransaction();
15: sess.saveOrUpdate(v);
16: trx.commit();
17: sess.close();
18: System.out.println("vehicle saved again: " + v);
When Hibernate reattaches the Vehicle object and brings it back into
the persistence context in the second session, it actually does the
unthinkable, it synchronizes (i.e. updates) every column of the
associated row without checking to see if it was updated by some other
concurrent process! Last in wins and no concurrency check!!!
1: Hibernate:
2: update
3: Vehicle
4: set
5: make='Chevy',
6: model='sedan',
7: vin=7890
8: where
9: id=1
Not JPA Standard
Wonder why I haven’t shown you the annotation form of this
non-version optimistic locking yet? If you are currently using
annotations to provide all of your object-database mappings, you will
also be disappointed to learn JPA does not support optimistic locking
outside of versioning (by number or timestamp). So there are no JPA
annotations to perform optimistic locking by field or dirty fields.
There is, however, a Hibernate annotations that can be used. When
mapping, add a parameter to the org.hibernate.annotations.Entity
annotation to specify how to perform optimistic locking (as shown here).
1: @Entity
2: @org.hibernate.annotations.Entity(dynamicUpdate = true,
3: optimisticLock = OptimisticLockType.ALL)
4: public class Vehicle {
5: // ...
6: }
Of course, this annotation requires a Hibernate import and more tightly couples your domain class to Hibernate.
It’s Slower
Additionally, note that using all-column or dirty-column checks as
part of the where-clause is a little slower. When updating a single
persistent object every so often, the speed may not be an issue. But in
systems dealing with large numbers of transactions, every millisecond
counts.
Wrap Up
As you can see, Hibernate can perform some real time and code-saving
work when it comes to optimistic locking. The several options allow for
you to pick a concurrent strategy in-line with your system architecture
and business needs. But under most circumstances, Hibernate optimistic
locking without a version or timestamp will not work for highly
concurrent, highly scalable applications, and doesn’t that pretty much
describe most enterprise application needs today? If you need more help
on Hibernate, contact Intertech and consider taking our
Complete Hibernate class.
No comments:
Post a Comment