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;

  1. create a new transaction (and a session)
  2. create a new domain object (persistable)
  3. fill your domain object with some erroneous data which will cause a hibernate exception
  4. attempt to save your object (which will result with a hibernate exception)
  5. close session, rollback transaction
  6. create a new transaction (and a session)
  7. fix the erroneous data at your object
  8. attempt, again, to save the object (expecting to be successful)

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() {
02     setComplete();
03     
04     Person personObj = createNewpersonObjInstance();
05     
06     System.out.println("original object identifier value : " + personObj.getId());
07     try {
08       // will cause to unique contraint violation exception
09       personObj.setUniqueName("AlreadyExistingName");
10       hibernateTemplate.saveOrUpdate(personObj);
11       endTransaction();
12       fail("we should get a ContraintError here because of bad data in object");
13     catch (Exception e) {
14       System.out.println("we got a ConstraintError at our first try. RolledBack db transaction (by Spring).");
15       System.out.println("error message : " + e.getMessage());
16     }
17     
18     System.out.println("saveOrUpdate method didn't rollback session state when the exception occured. so that "+
19         "our original object has its identifier value set now : " + personObj.getId());
20     
21     System.out.println("correct the bad data causing ConstraintError and try to save it in a new transaction.");
22     startNewTransaction();
23     setComplete();
24     try {
25       personObj.setUniqueName("NonExistingName");  // valid data - should be saved without any error
26       hibernateTemplate.saveOrUpdate(personObj);
27       endTransaction();
28     catch (Exception e) {
29       System.out.println("we shouldn't get any errors at this try since we've fixed bad data.");
30       System.out.println("error message : " + e.getMessage());
31       fail("we shouldn't get any errors at this try since we've fixed bad data.");
32     }
33     
34     System.out.println("saved object identifier value : " + personObj.getId());
35   }

The shortened result of the above test is;

Began transaction (1)default rollback = true

original object identifier value : null

insert into T_PERSON ....

SQL Error: 1, SQLState: 23000
ORA-00001: unique constraint (DBTEST.SYS_C0013109violated

Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: could not insert: [com.my.test.pojo.Person]
.....
Caused by: java.sql.SQLException: ORA-00001: unique constraint (DBTEST.SYS_C0013109violated
.....

we got a ConstraintError at our first try. RolledBack db transaction (by Spring).
error message : could not insert: [com.my.test.pojo.Person]; nested exception is 
  org.hibernate.exception.ConstraintViolationException: could not insert: [com.my.test.pojo.Person]

saveOrUpdate method didn't rollback session state when the exception occured. so that our original object has its 
  identifier value set now : 143
correct the bad data causing ConstraintError and try to save it in a new transaction.

Began transaction (2default rollback = true
Hibernate: update T_PERSON set ....
Could not synchronize database state with session
org.hibernate.StaleStateException: Unexpected row count: expected: 1
.....

we shouldn't get any errors at this try since we've fixed bad data.
error message : Unexpected row count: 0 expected: 1; nested exception is org.hibernate.StaleStateException: 
  Unexpected row count: 0 expected: 1
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">
    
    ...OTHER SESSIONFACTORY CONFIGURATIONS...
    
    <property name="eventListeners">
      <map>
        <entry key="merge">
          <bean class="org.springframework.orm.hibernate3.support.IdTransferringMergeEventListener"/>
        </entry>
      </map>
    </property>
  </bean>

After this; you can use Hibernate's merge method safely. This solution will provide you both:

  1. not associating your original object with session and save and return a copy of your unsaved object thus not throwing a StaleStateException
  2. setting identifier values of your original object via event listener, thus developers don't have to use the returning object

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

original object identifier value : null

insert into T_PERSON ....

SQL Error: 1, SQLState: 23000
ORA-00001: unique constraint (DBTEST.SYS_C0013109violated

Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: could not insert: [com.my.test.pojo.Person]
.....
Caused by: java.sql.SQLException: ORA-00001: unique constraint (DBTEST.SYS_C0013109violated
.....

we got a ConstraintError at our first try. RolledBack db transaction (by Spring).
error message : could not insert: [com.my.test.pojo.Person]; nested exception is 
  org.hibernate.exception.ConstraintViolationException: could not insert: [com.my.test.pojo.Person]

saveOrUpdate method didn't rollback session state when the exception occured. so that our original object has its 
  identifier value set now : 144
correct the bad data causing ConstraintError and try to save it in a new transaction.

Began transaction (2): rollback = true
Hibernate: TRY TO FIND THE GIVEN PERSON WITH ID VALUE...
insert into T_PERSON ....
Committed transaction

saved object identifier value : 145

Hope it helps,




Your Option (Login or Post by anonymous)