Hibernate is a persistence framework for Java. Among the many perks it purports to bring to the table is automatic versioning for objects in the database. That is, when saving an object to the database, it increments a version number. Any process that attempts to store a different version of the same object is rejected. This is all extremely flexible and can be added to a POJO using an annotation:
@Version private int version;
Nice ... a single annotation takes care of people overwriting each other's data. The exercise of handling the ensuing
StaleObjectStateException in the user interface is left up to the reader.
Now, imagine that we have an object -- call it a Book -- in memory and we render it to a web page. On that page is a button which attaches more information to the object -- say an Author -- then saves and rerenders the same book in the page. The user can add and save authors or change other book properties and save the book to exit edit mode for that book. Though split over multiple page requests, as far as Hibernate is concerned, the following actions occur on that object1:
book.save(); book.addAuthor(new Author(getNameOfAuthor())); book.save(); book.addAuthor(new Author(getNameOfAuthor())); book.save(); book.save(); // exit edit mode ...
This does not work. Hibernate raises a
StaleObjectException on the second execution of
save() because it never updated the version number in the object when it saved it the first time. That is, the automatically managed field
version is not synchronized with the object being saved when it is modified in the database. It's not like Hibernate doesn't know how to do this -- fields marked with the
@Id annotation are updated as expected.
At this point, there are two things to do:
@Versionisn't treated the same as
After a bit of initial debugging pursuing choice (1), it became clear that choice (2) would be much more efficient (not least because the line numbers in the accompanying sources didn't match the jar file).
The first step is to search online for this problem, but that was relatively fruitless, as no one else seemed to have had this problem, they didn't regard it as a problem or they didn't notice it yet. With hundreds, if not thousands, of companies using Hibernate, it's hard to believe that this feature is designed like this, or is fundamentally broken. The internet having failed us, we're left to fix this problem ourselves.
As some of you may already have been dying to point out, the quick and dirty way of fixing this is to simply update the version number by hand. At the risk of making any programming purists ill, here's that code:
book.save(); book.setVersion(book.getVersion() + 1);
However, this code assumes that it knows exactly how the automatic versioning feature of Hibernate works (or, rather, doesn't) and how future versions will work. Not liking that solution, we decide that we'll probably need to hit the database again; for that purpose there's the
...aaaaaand, the version number is still zero.
Taking a look at the database shows that the object with this id clearly has a version number of 2. Let's turn on query logging to see what Hibernate is doing when it executes a refresh on our object. In the
hibernate.cfg.xml file, set the following property (it's probably set to false in your default configuration):
<hibernate-configuration> <session-factory> ... <property name="hibernate.show_sql">true</property>
This time the console shows that Hibernate does indeed execute a select and does indeed select our version field. It, however, fails to apply that value to the object on which
refresh() was called.
That hack solution at the beginning of this section is starting to look mighty good. At this point, we're left with the alternative of reloading the object from the database in order to simulate the seemingly non-functional
refresh(). Reloading the object does get the correct version and leaves us with an object that we can use for further editing & saving operations.
book.save(); book = book.refresh(); book.addAuthor(new Author(getNameOfAuthor())); book.save(); book = book.refresh(); // exit edit mode ...
With this solution, however, we're forced to create a new object, reassigning the reference to
book. Though further reading online turned up references to tantalizing tidbits like
load(Object obj,Serializable id), it only generated
NonUniqueObjectExceptions; no combination of
evict() and flushing the session were able to avoid this. After more fruitless investigation in this vein -- and perusal of the Hibernate documentation, which, while good, doesn't link to actual examples of these methods in action -- the "fake" refresh outlined above was accepted as a general solution.
Be aware, however, that any other references to the object represented by
book are not updated and will still have the wrong
version. In straightforward web applications, where the object is primarily referenced from a single page object, this is less likely to be the case. Applications with more sophisticated operations -- for instance, where the reference is part of a graph of objects being edited -- the
refresh() outlined above is not a proper solution.
For the purposes of this discussion, assume that calls to
refresh() execute the similarly named functions on the Hibernate session ... these assumed shortcut functions make the code easier to read.
Using Java 1.5, Hibernate 3.2↩
Sign up for our Newsletter