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:
- id – this is the primary key,
- name – this is some human-readable entity name,
- content – some arbitrary field holding a string – it simulates the ‘advanced attribute’ which e.g. is calcuated during persistence/merging time and can result in errors.
- code – holds either OK or ERROR strings – defines if the advanced attributes were successful persisted or not,
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:
- Persist the E with its basic attributes,
- Try to update it with fragile advanced attributes,
- If
PersistenceException
was thrown from step 2. – catch it, set the ‘code’ attribute to “ERROR” and clear all advanced attributes (they caused an exception), - 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 the 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:
UpdateException
is an@ApplicationException
that extends Exception (so it isrollback=false
by default). It is used to inform that the update operation has failed. As an alternative you could change thetryMergingEntity(-)
method signature to return boolean instead of void. This boolean could describe if the update was successful or not.self
is a self reference to our own EJB. This is a required step to use EJB Container proxy which makes @TransactionAttribute of the called method work. As an alternative you could useSessionContext#getBusinessObject(clazz).tryMergingEntity(entity)
.- The
em.merge(entity)
is crucial. We are starting new transaction intryMergingEntity(-)
so the entity is not in persistence context. - There is no need for any other merge or flush in this method. The tx has not been rolled back so the regular features of CMT approves, meaning that all changes to the entity will be automatically flushed during tx commit.
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.
Reference: JPA and CMT – Why Catching Persistence Exception is Not Enough? from our JCG partner Piotr Nowicki at the Piotr Nowicki's Homepage blog.
Source : feedproxy[dot]google[dot]com
No comments:
Post a Comment