Summary: Hibernate users should be aware of saveOrUpdate method if they continue to use the same persistent object even if a transaction failed at some point.
Details:
Suppose you have a persistent object bound to your web(like JSF) views. Entered some data (which will lead to a db ConstraintViolationException) and tried to save it (at your DAO service) by using saveOrUpdate method. As we expected, it will throw a ConstraintViolationException and you'll rollback the transaction.
Then, go back to the entry page, correct the wrong field value at the same object, and try to save it again. You'll get a StaleStateException since saveOrUpdate method assigned identifier values automatically to your new object when you attempt to save it first. Later, when the save operation failed, it didn't roll back your object's state to its initial state. The summary of the flow causing this error is as below;
After 8th step, you will get a StaleStateException... Because we've opened a transaction and a hibernate session. Then made changes on them. Then, when an exception occured, we've rolled back the transaction thus db changes. But changes in our hibernate session didn't roll back. So that our persistent object now has its identifier values set even though we got an exception and rolled back the transaction. As a result; when i fixed my object's data and try to save it again, hibernate can not find the matching object in session (since now it has an identifier set) and will throw a StaleStateException.
The test case for the above scenario is as below (keep in mind i'm working on hibernate through Spring services but this doesn't matter for the situation);
01 public void testSaveOrUpdateThrowingStaleStateWhenDBExceptionOccuredAtFirstTry() {
|
The shortened result of the above test is;
Began transaction (1): default rollback = true
|
| Java2html |
I've googled so much for the solution. As seen, there are lots of users suffering from this error and dropping bugs and forum conversations about the problem. All they ask for is simple: my persistent object's identifier field was empty(it was a new record). Hibernate set it when i tried to save it. So, when i got an exception, hibernate must be responsible from rolling back those newly assigned identifier value(s) from the object(s). How can i roll back my object to initial state? Because, i don't know what the persistence layer is doing on my object while saving it.
But hibernate guys' response for those screams is simply incomprehensible: Rejected and Closed! They say; developers are responsible to "rollback the domain object's state to an acceptable level". This is ridiculous since developers don't know anything about what the persistence layer is doing on the objects at background.
Related issues and threads are here:
Anyway, if we come to the solution::
After some search and chat on the problem, a friend of mine advised me to look at the merge method. As you may know, merge method is different from saveOrUpdate as it doesn't associate the given (original) object with the session directly. Instead, it creates a copy of your persistent object and, after the persistence operation, returns that copied object which is associated with the session.
Its javadoc says "...If the given instance is unsaved, save a copy of and return it as a newly persistent instance...". That is the point; our desired functionality was this. merge method won't throw StaleStateException for my unsaved (but ids' set) objects. This was the fist part of solution. To complete the solution, we need one one more shot - go on with the next paragraph.
Now with the usage of merge method, if the given instance is unsaved, hibernate will save a copy of and return it as a newly persistent instance - won't throw a StaleStateException.
This means; hibernate won't set our original object's identifier value, it will return the copy of sent object with identifier values set. This requires developers to use always merge method's returning object instead of the sent (original) object. To protect the developers from this job it will be better if we set our original object's identifier field somehow. To do this, i have used another extension point of Hibernate - namely event listeners of session factory.
You can add some listeners to your Hibernate session factory configuration for some event types(see EventListeners for event types). To set the original object's identifier field after merge operation, the only thing you should do is writing your own event listener which will extend from DefaultMergeEventListener and set the original object's identifier with the persistent one's at the overridden entityIsTransient method. Guess what? There is a JEE framework whose job is to ease such enterprise level development - the Spring Framework. Thanks God, Spring guys (as happened most of the time) provided a solution for this need too.
If you add Spring's IdTransferringMergeEventListener to your session factory configuration, the identifier values of your newly saved object will be transferred to the corresponding original object that are passed into the merge method. Your session factory configuration will be like below;
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
|
After this; you can use Hibernate's merge method safely. This solution will provide you both:
After adding an event listener for the merge operations at your session factory configuration, you can see the above test will become green when replaced saveOrUpdate methods with merge methods. And result will be as below;
Began transaction (1): default rollback = true
|
Hope it helps,