Monday, May 21, 2007

Bug in Hibernate implementation of JPA: Persisting an entity

I've found a bug in the Hibernate implementation of JPA when persisting an entity.

I'm working with JPA using two different JPA implementations: Hibernate EM and Toplink Essential, and I've found certain interesting conditions (I'm to describe) upon which Hibernate throws an unexpected exception that I think it shouldn't.

The observation

The exception I obtained was this:

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist

And it is thrown when persisting an entity for the second time when the first time hadn't succeeded. The whole scenario is as follows:

Suppose a user is going to add a new entity such as a customer to the database but he/she enters some data invalid for the database (it violates some constraints, for instance a unique constraint) and therefore when the user clicks on the "add button" to add the customer, within the business logic, the persist method of the EntityManager throws a PersistenceException. In fact, with Hibernate, this exception is thrown in a lazy way when the transaction tries to commit.

Whatever the case, for the business logic to be robust, when the exception happens, it is caught and the user is informed, so he/she is allowed to modify the wrong data and to try again to add this same but modified entity.
So, when the user, after modifying some values of the customer proceeds again to click on the "add button", the business logic executes em.persist(entity) for the second time (with the same object entity). This time, although no constraint is violated in the database (the user worked it out), Hibernate throws the exception with the message: detached entity passed to persist.

The code of the business logic I was referring above is this:
public void addCustomer(Customer cust) throws ValidateException, CustomerException {

validate(cust);
EntityManagerFactory emf = PersistenceManager.getInstance().getEntityManagerFactory();
EntityManager em = emf.createEntityManager();

try {
EntityTransaction t = em.getTransaction();
try {
t.begin();
cust.setStartedDate(new Date());
em.persist(cust);
t.commit();
} finally {
if (t.isActive())
em.getTransaction().rollback();
}
} catch (PersistenceException ex) {
Throwable lastCause = ex;
while (lastCause.getCause() != null)
lastCause = lastCause.getCause();
if (lastCause.getMessage().startsWith("ORA-00001"))
throw new CustomerException("The Customer Id already exists.");
else
throw ex;
} finally {
em.close();
}
}
Note that I'm assuring both EntityManager em and transaction is closed before leaving this method!

My Customer Entity class is using a database sequence to feed its primary key:
@Entity
@Table(name = "CUSTOMER")
@SequenceGenerator(name="customerGen", sequenceName="SEQ_CUSTOMER", initialValue=1, allocationSize=1)
public class Customer implements Serializable {

@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="customerGen")
private Integer id;

...
}
And my client code (snippet from a JSF Backing bean) where the business logic is invoked is:
public String addCustomerAction() {

try {
model.addCustomer(customer);
return "done";
} catch (ValidateException ex) {
FacesContext.getCurrentInstance().getExternalContext().getRequestMap()
.put("validationErrors", ex.getMessages());
return "error";
} catch (CustomerException ex) {
FacesContext.getCurrentInstance().getExternalContext().getRequestMap()
.put("errors", ex.getMessages());
return "error";
}
}
Just after invoking the addCustomer() method for the second time the unexpected PersistenceException is thrown!
No exception should be thrown here, because the entity hasn't persisted yet (although we tried once but there was a rollback due to database constraint).
As I have checked, this works fine using Toplink Essential as the JPA engine.

The explanation

So, what is happening? Why am I obtaining this error if this method is invoked upon these conditions?

After some little research, I've discovered what should be happening with Hibernate and how to workaround with this problem.
Once you have tried to persist an entity and obtained an exception, the primary key of the entity is remembered and marked as a sort of "already used" state and therefore you shoudn't try to persist an entity with this primary key again: the workaround is based on assigning another primary key to the entity! This way you won't obtain the unexpected exception with the message: detached entity passed to persist.

My workaround client code (snippet from a JSF Managed bean)
public String addCustomerAction() {

try {
model.addCustomer(customer);
return "done";
} catch (ValidateException ex) {
FacesContext.getCurrentInstance().getExternalContext().getRequestMap()
.put("validationErrors", ex.getMessages());
return "error";
} catch (CustomerException ex) {
FacesContext.getCurrentInstance().getExternalContext().getRequestMap()
.put("errors", ex.getMessages());
customer.setId(null); // This is the line we need to workaround the problem with Hibernate!
return "error";
}
}
The actual value of the setId method doesn't matter as far as it is not the same. In this snippet it's assigned to null because this way the sequence will be use again to get the next value when persisting next time.

In My Humble Opinion, I think this is a bug, because if the transaction is rolled back, you should be able to work with the same primary key (as far as it isn't in the database yet).

I would like to know what is your opinion on this bug, so I encourage you to post any comments below!

I'm using the following version products:

  • Hibernate Core 3.2
  • Hibernate EntityManager 3.2.1 GA