EJB y @TransactionAttribute

por


Publicado en: marzo 19, 2019



En este artículo expondremos los diferentes tipos de @TransactionAttribute que se pueden utilizar en un EJB.

Normalmente al programar un EJB se tiende a olvidar el mecanismo que se emplea para administrar la propagación de las transacciones en un modelo CMT (Container Managed Transactions). En este artículo explicaremos los diferentes tipos que existen.

@TransactionAttribute


Esta anotación se puede establecer tanto a nivel de la clase o del método; y su valor por omisión es REQUIRED. ¿Qué quiere decir esto? Lo que significa es que si explícitamente no se define un atributo transaccional, los métodos del EJB serán por default REQUIRED.

REQUIRED

Veamos el siguiente ejemplo:

@Stateless
public class Service01 {
      public void insertar(Prueba prueba) {
         ....
       }

      public List<Prueba> getAll() {
          .....
      }
}

Este EJB, sería complementamente idéntico a haber escrito lo siguiente:

@Stateless
public class Service01 {
      @TransactionAttribute(TransactionAttributeType.REQUIRED)
      public void insertar(Prueba prueba) {
         ....
       }

      @TransactionAttribute(TransactionAttributeType.REQUIRED)
      public List<Prueba> getAll() {
          .....
      }
}

o bien

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service01 {

      public void insertar(Prueba prueba) {
         ....
       }

      public List<Prueba> getAll() {
          .....
      }
}

Debemos tener muy claro que el REQUIRED lo que quiere decir es que se tiene la garantía que el método será ejecutado dentro de una transacción. Si el método se invoca fuera de una transacción, el contenedor va a iniciar una nueva transacción, ejecutar el método y luego comprometer la misma o hacer un rollback según corresponda.

Consideremos el siguiente código:

@Stateless
public class Service01 {

    @PersistenceContext
    private EntityManager em;

    @Inject
    private Service02 servicioDos;

    public void insertar(Prueba prueba) {
        em.persist(prueba);
        try {
            servicioDos.testRequired();
        } catch (Exception ex) {
          //.....

        }
    }
}


@Stateless
public class Service02 {
    public void testRequired() throws DemoException {
        throw new DemoException();
    }

De acuerdo con el ejemplo, al momento de invocar el método insertar en el EJB Service01 se abre una transacción, se hace un persist de un nuevo registro en la tabla Prueba y de seguido se invoca al método testRequired del EJB Service02 -ese método no tiene ninguna anotación explícita, por lo que es REQUIRED.

El contenedor determina que es ejecutado dentro de una transacción (por lo que no se debe abrir una nueva) y en nuestro ejemplo se genera una Exception. Por tanto, eso causa que la transacción sea reversada como un todo, así que el intento de inserción sobre Prueba no queda registrado.

Es importante recordar que cualquier unchecked exception causaría un rollback, o bien excepciones configuradas como application exceptions, como la siguiente:

@ApplicationException(rollback = true)
class DemoException extends Exception {

    public DemoException() {
        super();
    }
}

De particular importancia es el rollback = true; lo que hace que la transacción actual sea reversada cuando se hace un throw de la misma.

REQUIRED_NEW

Si se emplea este atributo el contenedor pondría en pausa la transacción existente y crearía una nueva para la ejecución del método. Es decir, siempre se va a abrir una nueva transacción y el resultado de esa transacción no afectaría a la transacción que queda en pausa. Por ejemplo:

@Stateless
public class Service01 {

    @PersistenceContext
    private EntityManager em;

    @Inject
    private Service02 servicioDos;

    public void insertar(Prueba prueba) {
        em.persist(prueba);
        try {
            servicioDos.testRequiredNew();
        } catch (Exception ex) {
          //.....

        }
    }
}


@Stateless
public class Service02 {
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void testRequiredNew() throws RuntimeException {
     throw new RuntimeException("Required new");
  }

En este escenario, aunque el método testRequiredNew genera una excepción el dato que se registra en el metodo insertar de Service01 queda comprometido en la base de datos.

¿Porqué? Por que se abrió una transacción independiente para ejecutar la lógica del método testRequiredNew.

NEVER

En este caso, se tiene la certeza que el método nunca será ejecutado dentro de una transacción. Si quien invoca lo hace dentro de una transacción, el contenedor bloquearía la invocación y lanzaría una excepción. A manera de ilustración:

@Stateless
public class Service01 {

    @PersistenceContext
    private EntityManager em;

    @Inject
    private Service02 servicioDos;

    public void insertar(Prueba prueba) {
        em.persist(prueba);
        try {
            servicioDos.testNever();
        } catch (Exception ex) {
          //.....

        }
    }
}


@Stateless
public class Service02 {
  @TransactionAttribute(TransactionAttributeType.NEVER)
    public void testNever() throws RuntimeException {
        throw new RuntimeException("Required new");
    }

Si observamos el log, encontraríamos un error similar a este:

java.rmi.RemoteException: [EJB:011264]Method testNever() is marked TX_NEVER, but was called within a transaction.

NOT_SUPPORTED

Este atributo lo que establece es que se tiene la garantía que el método no será ejecutado dentro de una transacción. Si el invocador lo hace teniendo una transacción abierta, esta es suspendida; luego se ejecuta el método y posteriormente se continua la transacción del invocador.

SUPPORTS

Este atributo garantiza que se adoptará el estado transaccional del invocador. Es decir, puede ser invocado con o sin una transacción; el contenedor no hará nada para cambiarlo.

Por ejemplo:

@Stateless
public class Service02 {
  @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Prueba> getPruebas() throws RuntimeException {
        ......
    }

En lo personal, tradicionalmente seleccionó ese atributo para todos los métodos que retornan registros de la base de datos. Esto ya que no requiero que haya o no una transacción para lograr el resultado esperado.

MANDATORY

El comportamiento que se exhibe con MANDATORY es que debe existir una transacción abierta, de lo contrario una excepción sería lanzada por el contenedor. Es responsabilidad de quien invoca el EJB de suplir tal transacción.

@Stateless
public class Service01 {

    @PersistenceContext
    private EntityManager em;

    @Inject
    private Service02 servicioDos;

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public void insertar(Prueba prueba) {
        ....
        servicioDos.testMandatory();

    }
}

@Stateless
public class Service02 {
  @TransactionAttribute(TransactionAttributeType.MANDATORY)
    public void testMandatory() throws RuntimeException {
        ...
    }

Esto nos generaría el siguiente error:

javax.ejb.EJBTransactionRequiredException: Method testMandatory() is deployed as TX_MANDATORY, but it was called without a transaction.

Tomen en cuenta que en el Servicio01, el invocador tiene la anotación SUPPORTS, por lo que no ha abierto una transacción y en consecuencia se recibe ese error.

Conclusión

Esperamos que con este artículo se aclaren las dudas que generalmente surgen al emplear EJB y los diferentes comportamientos transaccionales que se tienen disponibles. Además de conocer el comportamiento por omisión en un EJB y determinar cual es el más adecuado para cada método que esten programando.

Autor del Artículo

Gerardo Arroyo Arce

Gerardo Arroyo Arce

Chief Technology Officer.

  • Ingeniero en Software. ITCR.
  • Master en Computación en Informática. UCR.
  • AWS Certified Solutions Architect - Associate
  • Oracle Certified Expert, JEE 6 Web Services Developer.
  • Oracle Certified Professional, Java SE 7 Programmer.
  • Oracle WebLogic Server 12c Certified Implementation Specialist.