Troubleshooting Hibernate

NonUniqueObjectException

What to do if you see an exception like the following

org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: [archive:org.openmrs.Role#System Developer]
at org.hibernate.engine.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:590)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:223)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:89)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:70)
at org.hibernate.impl.SessionImpl.fireSaveOrUpdate(SessionImpl.java:507)
at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:499)
at org.hibernate.engine.CascadingAction$5.cascade(CascadingAction.java:218)
at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:268)
...

This usually means that you have loaded an object into your hibernate session that is a duplicate of an object you also have around as a detached instance, and you then try to reattach the attached instance.

Here is an example of code that Does Not Work

// Opening a form, i.e. controller.formBackingObject()
User user = Context.getUserService().getUser(12);

// Next when the form is submitted, i.e. controller.onSubmit()
// at this point user is detached
for (String roleUserShouldHave : request.getParameterValues("role")) {
    Role role = Context.getUserService().getRole(roleUserShouldHave); // *This is the problem*
    user.addRole(role);
}

Context.getUserService().updateUser(user); // *This throws org.hibernate.NonUniqueObjectException*

The issue here is that in the problem line we are fetching an object from the database that we already have a detached reference to via the user object. This exception would not be thrown if updateUser was doing a session.merge(user), since that just copies the state of the user object into the database. But doing session.saveOrUpdate(user) tries to reattach the detached user object, which fails if one of its dependant detachd objects is already in the session as an attached object.

Here is one solution, meant to be illustrative rather than elegant:

for (String roleUserShouldHave : request.getParameterValues("role")) {
    Role role = null;
    for (Role test : user.getRoles())
        if (test.equals(roleUserShouldHave)
            role = test; // *it is safe to use the detached object*
    if (role == null)
        Role role = Context.getUserService().getRole(roleUserShouldHave); // *only do this if there is no detached object*
    user.addRole(role);
}

Could not synchronize database state with session, and other batch commit issues

When working with transactions that contain several changes, it is sometimes difficult to determine which sql stmt in the batch is the cause of problem. In these situations hibernate is often less then helpful: setting hibernate.show_sql=true in the runtime properties may help. In these situations, the helpful trick is to set the hibernate batch size to 0 in runtime properties via: hibernate.jdbc.batch_size=0

For example, without the setting, the error may look something like this:

ERROR - JDBCExceptionReporter.logExceptions(78) |2008-10-10 09:41:37,765| Field 'guid' doesn't have a default value
ERROR - AbstractFlushingEventListener.performExecutions(301) |2008-10-10 09:41:37,765| Could not synchronize database st
ate with session
        at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:103)
        at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:91)
        at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
...

With hibernate.jdbc.batch_size=0, the error message recorded is:

ERROR - JDBCExceptionReporter.logExceptions(78) |2008-10-10 09:41:37,765| Field 'guid' doesn't have a default value
ERROR - AbstractFlushingEventListener.performExecutions(301) |2008-10-10 09:41:37,765| Could not synchronize database st
ate with session
<b>org.hibernate.exception.GenericJDBCException: could not insert collection rows: [org.openmrs.User.roles#2408]</b>
        at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:103)
        at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:91)
        at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
...
Here is another instance of code where one may see NonUniqueObjectException

Patient patient = null;
Person person = null;

try {
            patient = Context.getPatientService().getPatient(patientId);
}

catch(ClassCastException ex){
            person = Context.getPersonService().getPerson(patientId);
            patient = new Patient(person);
}

....

Context.getPatientService().savePatient(patient); // It throws NonUniqueObjectException

The Problem appears when ClassCastException is thrown.

The context.getPatientService().getPatient(patientId) line is putting that object in the hibernate session, even when it throws a ClassCastException.
So after executing the ClassCastException handler two objects with the same patientId are loaded into the hibernate session. The first one is getting loaded with the Patient when getPatient(patientId) is called and the second one inside the ClassCastException handler with the Person when getPerson(patientId) is called. So Hibernate is right in calling the exception.

A possible solution to this could be to explicitly evict one of the objects from the hibernate session. The best approach would be to add this call in a finally block to guarantee that always one person object remains in the hibernate session with the given patientId.

Patient patient = null;
Person person = null;

try {
            patient = Context.getPatientService().getPatient(patientId);

}

catch(ClassCastException ex){
            person = Context.getPersonService().getPerson(patientId);
            patient = new Patient(person);
}

finally {

// removing the person object from session making sure only one object with a given id is present

 if (patient!=null)
         Context.evictFromSession(person);

}

...

Context.getPatientService().savePatient(patient);

This will possibly resolve the error.