Skip to main content

A simple method call: that is all it takes

Posted by mister__m on December 29, 2003 at 6:56 PM PST

Not having the burden of managing transactions by yourself - a.k.a Container Managed Transactions, CMT for short - is a compelling reason for using EJBs. Obviously, EJB is not the only technology that gives you that, but that's a entirely different discussion. Back to the point, the fact you don't have to call any transaction management method neither in java.sql.Connection nor in any class contained in javax.transaction makes a lot of people happy - especially those who already experienced the painful job of calling these manually. Although CMT is good and works - ok, in decent containers, let me be sincere here -, it is not magic and you have certain rules to obey so the container can do its job.

The most obvious thing - at least it should be - you should do is to specify which transaction attribute applies to each method in each EJB you wrote. There are a bunch of ways of doing it in the deployment descriptor, from specifying different attributes to each overloaded version of a method to using the same for all of them, and some containers have decent default values for when you don't declare anything, but I'm not going to cover this here. Today's point is more subtle than that and is something I've seen a considerable number of developers - including some good ones - not doing: calling setRollbackOnly().

javax.ejb.EJBContext, superclass of EntityContext, MessageDrivenContext, SessionContext is the interface that contains this method. The J2EE 1.4 javadoc description for this method is:

Mark the current transaction for rollback. The transaction will become permanently marked for rollback. A transaction marked for rollback can never commit. Only enterprise beans with container-managed transactions are allowed to use this method.

Note that only EJBs that use CMT - most of them - are allowed to use this method. But when are they required to? When it must be called? The answer is relatively simple, but astonishing for some people (from now on, everything here refers to CMT EJBs). Let's start to answer these questions by taking a look at section 17.3.4.2 of the recently published EJB 2.1 spec:

Typically, an enterprise bean marks a transaction for rollback to protect data integrity before throwing an application exception, because application exceptions do not automatically cause the container to rollback the transaction.

I highlighted the bold part and I wish it was already printed in bold in the pdf, actually. A lot of people read this section and simply ignore this important sentence. An application exception does not cause the container to rollback a transaction. It is a simple, but ignored fact. But what is an application exception? Section 18.1.1 defines it:

An application exception is an exception defined in the throws clause of a method of an enterprise bean’s home, component, message listener, or web service endpoint interface, other than the java.rmi.RemoteException.

Enterprise bean business methods use application exceptions to inform the client of abnormal application-level conditions, such as unacceptable values of the input arguments to a business method. A client can typically recover from an application exception. Application exceptions are not intended for reporting
system-level problems.

Although the explanation is somewhat deceiving - Java won't stop you from declaring any RuntimeException subclasses in yours throws clause, but that won't make them application exceptions -, I think you got the point. Transactions are not automatically rolled back when you throw your custom Exception-derived instance. The same does not apply to system exceptions, as section 18.2.2 points out:

The Bean Provider can rely on the container to perform the following tasks when catching a non-application exception:

  • The transaction in which the bean method participated will be rolled back.

So, if you throw a RuntimeException or a javax.ejb.EJBException to wrap a checked exception, you are safe. Let's see how we could perform the classical withdraw operation in a Entity Bean:

// Some code goes here

public BigDecimal withdraw(BigDecimal amount) throws InsufficientBalanceException {
  BigDecimal balance = getBalance();

   //Ooops, trying to do something that shouldn't be done
   if (balance.compareTo(amount) < 0) {
      // This call is necessary as the exception to be thrown does not derive from RuntimeException
      entityContext.setRollbackOnly();
      throw new InsufficientBalanceException(balance, amount);
   }

   balance = balance.subtract(amount);
   setBalance(balance);

   return balance;
}

// And more code goes here

Throwing a checked exception is not enough; you need to call setRollbackOnly() so things happen as you expect. It is a simple truth most of us ignore and maybe that's why sometimes there is some corrupt data in our tables - the container will commit any modifications you performed before throwing an application exception unless you've called setRollbackOnly(). If you don't call this method, throw application exceptions after modifying data and never seen this error, consider it to be a bug. Really. And start looking for another container, there are plenty out there. :-D It won't be any fun when your broken container rolls back a transaction that should be commited...

To end up, be aware that setRollbackOnly() does not work all the time. Section 17.6.2.8 explicitly says:

The container must throw the java.lang.IllegalStateException if the EJBContext.setRollbackOnly method is invoked from a business method executing with the Supports, NotSupported, or Never transaction attribute.

So, if all you are bean methods are configured with the Supports attribute, you there is no (standard) way to programatically rolling back transactions! It is creepy, scary and terrifying as it sounds! Pay attention to your transaction attributes! And do not forget to call setRollbackOnly() whenever it's necessary!

Related Topics >>