Search code examples
jakarta-eetransactionsejb

Nested method calls in EJBs attempting to start/create a new transaction in a nested method call


Let's consider two methods methodA() annotated with @TransactionAttribute(TransactionAttributeType.REQUIRED) (default) and methodB() annotated with @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) in a stateless EJB.

Making a nested call to methodB() through methodA() does not start/create a new transaction as it appears to be (regardless of what transaction attribute type is used the target method), since the nested call to methodB() from methodA() uses the this pointer/reference (i.e. the actual EJB instance) to invoke methodB() which is thus not intercepted by the proxy which is to be injected at run-time by the container and needed to setup the environment before calling a method.

Basic demonstration :

@Stateless
public class TestBean implements TestBeanService {

    @PersistenceContext
    private EntityManager entityManager;

    // At a glance, this method should cause an exception but it does not.
    @Override
    @TransactionAttribute(TransactionAttributeType.NEVER)
    public Long getRowCount() {
        return entityManager.createQuery("SELECT count(e) AS cnt FROM Employee e", Long.class).getSingleResult();
    }

    // This method is invoked by the application client.
    // making a nested call to getRowCount() using the "this" pointer.
    @Override
    public void test() {
        Long rowCount = getRowCount();
        System.out.println("rowCount : " + rowCount);
    }
}

Although the getRowCount() method is decorated with @TransactionAttribute(TransactionAttributeType.NEVER) which should cause an exception at a glance, it successfully returns the number of rows returned by the query.

► This is because the transaction started by the test() method is propagated (expanded) to getRowCount() i.e. everything happens within the same single transaction.


An exception would however, be thrown, if the getRowCount() method were to be invoked using a proxy instance obtained through javax.ejb.SessionContext. This modification is demonstrated by the following snippet.

@Stateless
public class TestBean implements TestBeanService {

    @PersistenceContext
    private EntityManager entityManager;

    @Resource
    private SessionContext sessionContext;

    @Override
    @TransactionAttribute(TransactionAttributeType.NEVER)
    public Long getRowCount() {
        return entityManager.createQuery("SELECT count(e) AS cnt FROM Employee e", Long.class).getSingleResult();
    }

    @Override
    public void test() {
        // Invocation to getRowCount() is done by a proxy instance. Hence, it causes an exception,
        // since the transaction started by this method is now not propagated to a subsequent call to getRowCount().
        Long rowCount = sessionContext.getBusinessObject(TestBeanService.class).getRowCount();
        System.out.println("rowCount : " + rowCount);
    }
}

Since the getRowCount() method uses TransactionAttributeType.NEVER, the above method call to getRowCount() causes the following exception to be thrown contradicting to the first case.

javax.ejb.EJBException: EJB cannot be invoked in global transaction

► This is because the transaction started by the test() method is now not propagated (expanded) to getRowCount() as happens in the first case because this method is now invoked through a proxy instance (thus, not directly through the this pointer as usual - the actual EJB instance).

Obtaining a proxy instance through javax.ejb.SessionContext and invoking a method on that proxy instance is a kind of hack. I do not think it is supposed to be used in real applications.

Is there any other way to start/create a new transaction in a nested method call whenever needed?


Solution

  • Transaction attributes are honored only when you access the bean via interface/proxy, not as internal method, as you observed. So in addition to access it via SessionContext you could also have a reference: @EJB TestBeanService serviceBean in your class and access it via serviceBean.getRowCount() (also kind of a hack).