JPA and CMT -- Why Catching Persistence Exception is Not Enough?

Being in EJB and JPA world using CMT (Container Managed Transactions) is very comfortable. Just define few annotations to demarcate transaction boundary (or use the defaults) and that’s it – no fiddling with manual begin, commit or rollback operations.

One way to rollback your transaction is to throw non-application exception (or application exception with rollback = true) from your EJB’s business method.

It seems simple: if during some operation there is a possibility that an exception will be thrown and you don’t want to rollback your tx than you should just catch this exception and you’re fine. You can now retry the volatile operation once again within the same, still active transaction.

Now its all true for application exceptions thrown from user’s components. The question is – what with exceptions thrown from other components? Like JPA’s EntityManager throwing a PersistenceException?

And that’s where the story begins.

What We Want to Achieve

Imagine the following scenario:

You have an entity named E. It consists of:

You want to persist E.
You assume that basic attributes of E will always be successfully persisted.
The advanced attributes, however, requires some additional calculations or operations which might result in e.g. a constraint violation being thrown from the database.
If such situation occur, you still want to have E persisted in the database (but only with basic attributes filled in and the code attribute set to “ERROR”).

In other words this is what you could think of:

  1. Persist the E with its basic attributes,
  2. Try to update it with fragile advanced attributes,
  3. If PersistenceException was thrown from step 2. – catch it, set the ‘code’ attribute to “ERROR” and clear all advanced attributes (they caused an exception),
  4. Update E.

Naive solution

Moving to EJB’s code this is how you might try doing it (assume default TransactionAttributes):

public void mergeEntity() {
    MyEntity entity = new MyEntity("entityName", "OK", "DEFAULT");

    em.persist(entity);

    // This will raise DB constraint violation
    entity.setContent("tooLongContentValue");

    // We don't need em.merge(entity) - our entity is in managed mode.

    try {
        em.flush();  // Force flushing to occur now, not during method commit.
    } catch (PersistenceException e) {  
        // Clear the properties to be able to persist the entity.
        entity.setContent("");
        entity.setCode("ERROR");

       // We don't need em.merge(entity) - our entity is in managed mode.
    }
}

What’s Wrong With This Example?

Catching of PersistenceException thrown by an EntityManager is not going to prevent transaction from rolling back. It’s not like that not caching an exception in your EJB will make the tx marked for rollback. It’s the throwing of non-application exception from EntityManager marking the tx to rollback. Not to mention that a resource might by its own mark a tx for rollback in its internals.
It effectively means your application doesn’t really have control over such tx behavior.

Moreover, as a result of transaction rollback, our entity has been moved to detached state. Therefore some em.merge(entity) at the end of this method would be required.

Working Solution

So how you can deal with this automatic transaction rollback? Because we’re using CMT our only way is to define another business method that will start a fresh transaction and perform all fragile operations there. This way even if PersistenceException will be thrown (and caught) it will mark only the new transaction to be rolled back. Our main tx will be untouched.

Below you can see some code sample from here (with logging statements removed for brevity):

public void mergeEntity() {
    MyEntity entity = new MyEntity("entityName", "OK", "DEFAULT");

    em.persist(entity);

    try {
        self.tryMergingEntity(entity);
    } catch (UpdateException ex) {
        entity.setContent("");
        entity.setCode("ERROR");
    }
}

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void tryMergingEntity(final MyEntity entity) throws UpdateException {
    entity.setContent("tooLongContentValue");

    em.merge(entity);

    try {
        em.flush();
    } catch (PersistenceException e) {
        throw new UpdateException();
    }
}

Mind that:

Let’s emphasize once again the bottom line: If you catch an exception it doesn’t mean your current transaction hasn’t been marked for rollback. PersistenceException is not an ApplicationException and will make your tx rollback despite if you catch it or not.

JTA BMT Solution

All the time we were talking about CMT. What about JTA BMT? Well, as a bonus find the below code which shows how to deal with this problem with BMT (accessible here as well):

public void mergeEntity() throws Exception {
    utx.begin();
    MyEntity entity = new MyEntity("entityName", "OK", "DEFAULT");
    em.persist(entity);
    utx.commit();

    utx.begin();
    entity.setContent("tooLongContentValue");

    em.merge(entity);

    try {
        em.flush();
    } catch (PersistenceException e) {
        utx.rollback();

        utx.begin();
        entity.setContent("");
        entity.setCode("ERROR");

        em.merge(entity);
        utx.commit();
    }
}

With JTA BMT we can do this all in just one method. This is because we control when our tx begins and commits/rollbacks (take a look at those utx.begin()/commit()/rollback().

Nevertheless, the result is the same – after throwing PersistenceException our tx is marked for rollback and you can check it using UserTransaction#getStatus() and comparing it to one of the constants like Status.STATUS_MARKED_ROLLBACK.

You can check the whole code at my GitHub account.