En los negocios, una transacción incluye un intercambio entre dos partes. Cuando compras un periódico, intercambias dinero por un objeto; cuando trabajas para una compañía, intercambias conocimiento y tiempo por dinero. Si nos encontramos en uno de estos intercambios, siempre tenemos cuidado de asegurarnos que no sucede nada extraño. Si le damos al quiosquero un billete de 5€, esperamos que nos devuelva 4€ junto con el periódico, que vale 1€. Monitorizamos la seguridad de la transacción para asegurarnos de que cumple todas las restricciones que conlleva.
En software de negocios, una transacción incluye el concepto de un intercambio comercial. Una transacción de un sistema de negocios (transacción para abreviar) es la ejecución de una unidad-de-trabajo que accede uno o más recursos compartidos, normalmente bases de datos. Una unidad-de-trabajo es un conjunto de actividades que se relacionan mutuamente y que deben ser realizadas completamente. Por ejemplo, una operación de reserva de un billete de avión en un sistema informático puede estar formada por la selección del asiento a reservar, el cargo en una tarjeta de crédito y la generación de un billete. Todas estas acciones forman una unidad-de-trabajo que no puede romperse.
Las transacciones forman parte de distintos tipos de sistemas. En cada transacción el objetivo es el mismo: ejecutar una unidad-de-trabajo que resulte en un intercambio fiable. He aquí algunos ejemplos de sistemas de negocios que usan transacciones:
Cajeros automáticos
Los cajeros automáticos son un ejemplo típico de sistema en el que es fundamental el uso de transacciones. Cuando sacas dinero del cajero, por ejemplo, se debe chequear que tienes dinero suficiente en la cuenta corriente, después se debe entregar el dinero y por último se debe realizar un cargo en la cuenta.
Compra on-line
En una compra on-line también se debe hacer un uso intensivo de las transacciones. Cuando realizas una compra on-line debes de proporcionar el número de tarjeta de crédito, éste debe validarse y después debe cargarse el precio de la compra. Luego se debe emitir un pedido al almacén para que realice el envío de la compra. Todas estas acciones deben ser una unidad-de-trabajo, una transacción que se debe ejecutar de forma indivisible.
Los sistemas que necesitan usar transacciones normalmente son complejos y realizan operaciones que conllevan el uso grandes cantidades de datos. Las transacciones deben por ello preservar la integridad de los datos, lo que significa que todas las operaciones que forman las transacciones deben funcionar perfectamente o la que la transacción no se debe ejecutar en absoluto.
En el campo de la gestión de transacciones se han identificado cuatro características que las transacciones deben cumplir para que el sistema sea considerado seguro. Las transacciones deben ser atómicas, consistentes, aisladas y duraderas (ACID, en inglés). A continuación describimos estos términos:
Atómica
Para ser atómica, una transacción debe ejecutarse totalmente o no ejecutarse en absoluto. Esto significa que todas las tareas dentro de una unidad-de-trabajo deben ejecutarse sin error. Si alguna de estas tareas falla, la transacción completa se debe abortar y todos los cambios que se han realizado en los datos deben deshacerse. Si todas las tareas se ejecutan correctamente, la transacción se comete (commit), lo que significa que los cambios relizados en los datos se hacen permanentes o duraderos.
Consistente
La consistencia es una característica transaccional que debe ser impuesta tanto por el sistema transaccional como por el desarrollador de la aplicación. La consistencia se refiere a la integridad del almacén de datos. El sistema transaccional cumple su obligación de consistencia asegurando que una transacción es atómica, aislada y duradera. El desarrollador de la aplicación debe asegurarse que la base de datos tiene las restricciones apropiadas (claves primarias, integridad referencial, y otras) y que la unidad-de-trabajo, la lógica de negocio, no resulta en datos inconsistentes (esto es, los datos no se corresponden con lo que representan del mundo real). En una transferencias entre cuentas, por ejemplo, un cargo en una cuenta debe ser igual a un ingreso en otra.
Aislada
Una transacción debe poder ejecutarse sin interferencia de otros procesos o transacciones. En otras palabras, los datos a los que accede una transacción no pueden ser modificados por ninguna otra parte del sistema hasta que la transacción se completa.
Duradera
Durabilidad significa que todos los cambios en los datos realizados durante el curso de una transacción deben escribirse en algún tipo de almacenamiento físico antes de que la transacción concluya con éxito. Esto asegura que los cambios no se pierden si el sistema se cae.
Supongamos el siguiente método de un EJB que llamamos TravelAgent
public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState(); } try { ReservationHomeLocal resHome = (ReservationHomeLocal) jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal"); ReservationLocal reservation = resHome.create(customer, cruise, cabin, price); Object ref = jndiContext.lookup ("java:comp/env/ejb/ProcessPaymentHomeRemote"); ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote) PortableRemoteObject.narrow(ref, ProcessPaymentHomeRemote.class); ProcessPaymentRemote process = ppHome.create(); process.Ñ?byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); return ticket; } catch(Exception e) { throw new EJBException(e); } }
¿Es fiable el método? Una primera medida de la fiabilidad del EJB TravelAgent es su atomicidad: ¿asegura que la transacción se ejecuta completamente o no se ejecuta en absoluto? Para responder debemos concentrarnos en las tareas críticas que modifican o crea información nueva. En el método bookPassage(), se crea un EJB Reservation, el EJB ProcessPayment realiza un cargo en una tarjeta de crédito, y se crea un objeto TicketDO. Todas estas tareas deben tener éxito para que la transacción completa lo tenga a su vez.
Para entender la importancia de la característica de atomicidad podríamos imaginar qué sucedería si cualquiera de estas tareas fallara. Si, por ejemplo, la creación de un EJB Reservation fallara pero todas las otras tareas tuvieran éxito, el cliente terminaría probablemente expulsado del crucero o compartiendo el camarote con un extraño. En lo que concierne a la agencia de viajes, el método bookPassage() habría sido ejecutado con éxito porque se habría generado un TicketDO. Si se genera un billete sin la creación de una reserva, el estado del sistema de negocio se convierte en inconsistente con la realidad, porque el cliente ha pagado por un billete pero la reserva no se ha registrado. De la misma forma, si el EJB ProcessPaymente falla al cargar la tarjeta de crédito del cliente, el cliente obtiene un crucero gratis. Seguro que él se quedará contento, pero gerencia no. Por último, si el TicketDO no se crea nunca, el cliente no tendrá ningún registro de la transacción y probablemente no podrá subir al crucero.
Por ello, la única forma de que pueda completarse la operación bookPassage() es si todas sus partes críticas se ejecutan con éxito. Si algo va mal, el proceso completo debe abortarse. Abortar una transacción requiere más que simplemente no finalizar las tareas; además, todas las tareas dentro de la transacción deben deshacerse. Si, por ejemplo, la creación de los EJB Reservation y ProcessPayment se realiza con éxito, pero la creación del TicketDO falla, los registros reservation y payment no deben añadirse a la base de datos.
Para que la operación bookPassage() sea completamente segura debe cumplir los otros requisitos de una transacción: debe ser consistente, aislada y duradera.
Para mantener la consistencia de las operaciones desde el punto de vista de la lógica del negocio, es necesario que se cumplan las otras tres propiedades y además que el desarrollador de la aplicación sea estricto a la hora de aplicar las restricciones de integridad en toda la implementación de la aplicación. Por ejemplo, de nada serviría que el sistema transaccional asegurase la atomicidad de la operación bookPassage() si el desarrollador no incluyera dentro del método una llamada a la consulta de la tarjeta de crédito y devolviera directamente el TicketDO. Desde el punto de vista del negocio, la transacción habría fallado, ya que se ha emitido un ticket sin realizar un cobro.
El aislamiento de la operación tiene que asegurar que otros procesos no van a modificar los datos de los EJBs mientras que la transacción está desarrollándose.
Por último, la durabilidad de la transacción obliga a que todas las operaciones hayan sido hechas persistentes antes de dar la transacción por terminada.
Asegurarnos que las transacciones se adhieren a los principios ACID requiere un diseño cuidadoso. El sistema tiene que monitorizar el progreso de una transacción, para asegurarse de que todo funciona correctamente, de que los datos se modifican de forma correcta, de que las transacciones no interfieren entre ellas y de que los cambios pueden sobrevivir a una caída del sistema. Comprobar todas estas condiciones conlleva un montón de trabajo. Afortunadamente, la arquitectura EJB soporta de forma automática el manejo de transacciones.
Una de las ventajas principales de la arquitectura Enterprise JavaBeans es que permite la gestión declarativa de transacciones. Sin esta característica las transacciones deberían controlarse usando una demarcación explícita de la transacción. Esto conlleva el uso de APIs bastantes complejos como el Objecto Transaction Service (OTS) de OMG, o su implementación Java, el Java Transaction Service (JTS). La demarcación explícita es difícil de usar correctamente para los desarrolladores, sobre todo si no están habituados a la programación de sistemas transaccionales. Además, la demarcación explícita requiere que el código transaccional se escriba junto con la lógica de negocio, lo que reduce la claridad del código y, más importante, crea objetos distribuidos inflexibles. Una vez que la demarcación de la transacción está grabada en el objeto de negocio, los cambios en la conducta de transacción obligan a cambios en la misma lógica de negocio.
Con la gestión declarativa de transacciones, la conducta transaccional de los EJBs puede controlarse usando el descriptor del despliegue, que establece atributos de transacción para los métodos individuales del enterprise bean. Esto significa que la conducta transaccional de un EJB puede modificarse sin cambiar la lógica de negocio del EJB. Además, un EJB desplegado en una apliación puede definirse con una conducta transaccional distinta que la del mismo bean desplegado en otra aplicación. La gestión declarativa de las transacciones reduce la complejidad del manejo de las transacciones para los desarrolladores de EJB y de aplicaciones y hace más sencilla la creación de aplicaciones transaccionales robustas.
El resto de este tema examina cómo los EJB soportan implícitamente las transacciones a través de atributos declarativos de transacciones.
El alcance de una transacción es un concepto crucial para comprender las transacciones. En este contexto, el alcance una transacción consiste en aquellos EJBs de entidad y de sesión que están participando en una transacción particular.
En el método bookPassage() del EJB TravelAgent, todos los EJBs que participan son parte del mismo alcance de transacción. El alcance de la transacción comienza cuando el cliente invoca el método bookPassage() del EJB TravelAgent. Una vez que el alcance de la transacción ha comenzado, éste se propaga a los dos EJB que se crean: Reservation EJB y ProcessPayment EJB.
Como ya hemos comentado, una transacción es una unidad-de-trabajo constituida por una o más tareas. En una transacción, todas las tareas que forman la unidad-de-trabajo deben ser un éxito para que la transacción en su totalidad tenga éxito; la transacción debe ser atómica. Si alguna tarea falla, las actualizaciones realizadas por las otras tareas deben desacerse. En EJB, las tareas se expresan como métodos de los enterprise bean, y una unidad-de-trabajo consiste en un conjunto de invócaciones a métodos los enterprise bean. El alcance de la transacción incluy todos los EJB que participan en al unidad de trabajo.
Es fácil trazar el alcance de una transacción siguiendo el hilo de ejecución. Si la invocación del método bookPassage() comienza una transacción, entoces de forma lógica, la transacción termina cuando el método se completa. El alcance de la transacción bookPassage() incluiría los EJB TravelAgent, Reservation y ProcessPayment.
Una transacción puede terminar si se arroja una excepción mientras que el método bookPassage() está en ejecución. La excepción puede arrojarse desde uno de los otros EJBs o desde el mismo método bookPassage(). Una excepción puede causar o no causar rollback (vuelta a los valores iniciales de los datos) dependiendo de su tipo. Lo veremos más adelante.
Como desarrolladores de aplicaciones, no es necesario que controlemos explícitamente las transacciones cuando estamos usando un servidor EJB. Los servidores EJB pueden manejar las transacciones de forma implícita, basándose en los atributos transaccionales establecidos para los EJB en el momento del despliegue. La posibilidad de especificar cómo participan en las transacciones los objetos de negocio mediante programación basada en atributos es una característica común de los monitores de transacciones, y una de las características más importantes del modelo de componentes EJB.
Cuando un EJB se despliega, podemos establecer su atributo de transacción a uno de los siguients valores:
La especificación EJB 2.0 aconseja fuertemente que los beans de entidad con persistencia gestionada por el contenedor usen sólo los atributos Required, RequiresNew y Mandatory. Esta restricción asegura que todos los accesos a bases de datos suceden en el contexto de una transacción, lo cual es importante cuando el contenedor está gestionando automáticamente la persistencia.
Podemos establecer un atributo de transacción para el EJB completo (en cuyo caso se aplica a todos los métodos) o establecer distintos atributos de transacción para los métodos individuales. Lo primero es mucho más sencillo y conlleva menos riesgo de errores, pero lo segundo ofrece mayor flexibilidad. Los fragmentos de código de los siguientes apartados muestran cómo establecer los atributos de transacciones de un EJB en el descriptor del despliegue del EJB.
En el descriptor XML del despliegue, un elemento <container-transaction> especifica los atributos de transacción para los EJBs descritos en el descriptor de despliegue:
<ejb-jar> ... <assembly-descriptor> ... <container-transaction> <method> <ejb-name>TravelAgentEJB</ejb-name> <method-name> * </method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> <container-transaction> <method> <ejb-name>TravelAgentEJB</ejb-name> <method-name>listAvailableCabins</method-name> </method> <trans-attribute>Supports</trans-attribute> </container-transaction> ... </assembly-descriptor> ... </ejb-jar>
Este descriptor de despliegue especifica los atributos de transacción para el EJB TravelAgent. Cada elemento <container-transaction> especifica un método y el atributo de transacción que debería aplicarse a ese método. El primer elemento <container-transaction> especifica que todos los métodos tengan por defecto un atributo de transacción de Required; el carácter * es un comodín que indica todos los métodos del EJB TravelAgent. El segundo elemento <container-transaction> hace caso omiso del atributo por defecto para especificar que el método listAvailableCabins() tendrá un atributo de transacción Supports. Tenemos que especificar a qué EJB nos estamos refiriendo con el elemento <ejb-name>, ya que un descriptor XML de despliegue puede contener muchos EJBs.
Vamos a definir ahora los atributos de transacción que hemos declarado previamente. Tal y como comentamos arriba, los atributos recomendados por la especificación EJB 2.0 son Required, RequiresNew y Mandatory
Required
Este atributo significa que el método del enterprise bean debe invocarse dentro del alcance de una transacción. Si el cliente o el EJB que realiza la llamada es parte de una transacción el EJB con el atributo Required automáticamente se incluye en el alcance de esa transacción. Si, sin embargo, el cliente o EJB que realiza la llamada no está incluído en una transacción, el EJB con el atributo Required comienza su propia transacción. Esta nueva transacción concluye cuando el EJB termina.
RequiresNew
Este atributo significa que se debe comenzar siempre una nueva transacción. Independiéntemente de si el cliente o EJB que realiza la llamada es parte de una transacción, un método con el atributo RequiresNew siempre comienza una nueva transacción. Si el cliente que realiza la llamada ya está incluido en una transacción, esa transacción se suspende hasta que la llamada al método con el atributo RequiresNew finaliza. En ese momento la transacción original vuelve a estar activa.
Mandatory
Este atributo significa que el método del enterprise bean debe siempre ser parte del alcance de la transacción del cliente que realiza la llamada. Si este cliente o EJB no es parte de una transacción, la invocación fallará, arrojándose una excepción javax.transaction.TransactionRequiredException a los clientes remotos o una excepción javax.ejb.TransactionRequiredLocalException a los clientes locales.
Para ilustrar el impacto de los atributos de transacción sobre los métodos del enterprise bean, miraremos una vez más al método bookPassage() del EJB TravelAgent.
Para que bookPassage() se ejecute como una transacción con éxito, tanto la creación del EJB Reservation como el cargo a la tarjeta de crédito deben también terminar con éxito. Esto significa que ambas operaciones deben incluirse en la misma transacción. Si alguna operación falla, la transacción completa falla. Podríamos haber especificado un atributo de transacción Required como el atributo por deefecto para todos los EJBs incluidos, porque ese atributo refuerza la política deseada de que todos los EJBs deben ejecutarse dentro de una transacción y por ello asegura la consistencia de los datos.
Como un monitor de transacciones, el servidor EJB vigila cada llamada a un método en la transacción. Si cualquiera de las actualizaciones falla, todas las actualizaciones a todos los EJBs serán rebobinadas o recuperadas (rolled back). Una recuperación (rollback) es como un comando undo. Si hemos trabajado con bases de datos relacionales, el concepto de una recuperación debe sernos familiar. Una vez que se ejecuta una actualización (update), podemos o bien asegurar esa actualización (commit) o bien recuperar los datos iniciales (rollback). El uso de EJBs transaccionales nos proporciona el mismo tipo de control rollback/commit. Por ejemplo, si el EJB Reservation no pudiera crearse, el cargo realizado por el EJB ProcessPayment es descontado (rolled back).
En los casos en los que el contenedor gestiona implicitamente la transacción, las decisiones de commit y rollback se manejan de forma automática. Supongamos que el EJB TravelAgent se crea y se usa en el cliente como sigue:
TravelAgent agent = agentHome.create(customer); agent.setCabinID(cabin_id); agent.setCruiseID(cruise_id); try { agent.bookPassage(card,price); } catch(Exception e) { System.out.println("Transaction failed!"); }
Más aún, asumamos que el método bookPassage() tienen un atributo de transacción RequiresNew. En este caso, el cliente que invocal el método bookPassage() no es parte de la transacción. Cuando se invoca a bookPassage() en el EJB TravelAgent, se crea una nueva transacción, tal y como dicta el atributo RequiresNew. Esto significa que el EJB TravelAgent se registra en el gestor de transacciones del servidor de EJB, el cual gestionará la transacción automáticamente. El gestor dfe transacciones coordina transacciones, propagando el alcance de la transacción desde un EJB al siguiente para asegurarse de que todos los EJBs tocados por una transacción se incluyen en la unidad-de-trabajo de la transacción. De esta forma, el gestor de transacciones puede monitorizar las actualizaciones realizadas por cada enterprise bean y decidir, basándose en el éxito de estas actualizaciones, si hacer permanentes los cambios hechos por todos los enterprise beans en las bases de datos o si echarlos atrás y deshacerlos. Si una excepción del sistema se arroja en el método bookPassage(), la transacción es automática deshecha (rolled back).
Cuando el método byCredit() se invoca dentro del método bookPassage(), el EJB ProcessPaymet se registra en el gestor de transacciones bajo el contexto transaccional que se creó para el EJB TravelAgent. Cuando se crea el nuevo EJB Reservation, también se registra en el gestor de transacciones bajo la misma transacción. Cuando se registran todos los EJBs y se realizan todas las actualizaciones, el gestor de transacciones chequea todo para asegurarse de que sus actualizaciones funcionarán. Si uno de los EJB devuelve un error o falla, los cambios realizados por los EJB ProcessPayment o Reservation se deshacen por el gestionador de transacciones.
Además de gestionar las transacciones en su mismo entorno, un servidor EJB puede cooordinarse con otros sistemas transaccionales. Si, por ejemplo, el EJB ProcessPayment realmenet viniera de un servidor EJB distinto, los dos servidores EJB cooperarían para gestionar la transaccióno como una unidad-de-trabajo. Esto se llama una transacción distribuída.
Una transacción distribuída es mucho más complicada, y requiere lo que se llama two-phase commit (2-PC o TPC). 2-PC es un mecanismo que permite que una transacción sea gestionada a través de distintos servidore y recursos (por ejemplo, bases de datos y proporcionadores JMS). Los detalles de un sistema 2-.PC están más allá del alcance de este tema. Si se soportan las transacciones distribuidas, se soportará el mismo protocolo para propagarlas. No nos daremos cuenta, como desarrolladores de EJBs o de aplicaciones, de las diferencias entre transacciones locales y distribuidas.
En la persistencia gestionada por el contenedor EJB 2.0, las relaciones basadas en colecciones pueden accederse sólo dentro de una única transacción. En otras palabras, no es legal obtener un objeto Collection de un campo de relación basado en colleciones en una transacción y usarlo en otra.
Por ejemplo, si un enterprise bean accede a otro campo de relación basado en una colección a través de su interface local, el objeto Collection devuelto del método de acceso sólo puede usarse dentro de la misma transacción:
public class HypotheticalBean implements javax.ejb.EntityBean { public void methodX(CustomerLocal customer) { Collection reservations = customer.getReservations(); Iterator iterator = reservations.iterator; while(iterator.hasNext()){ ... ... } ... }
Si se hubiera declarado como RequiresNew el atributo del método getReservations() del EJB Customer, cualquier intento de invocar algún método en el objeto Collection, incluyendo el método iterator(), resultará en una java.lang.IllegalStateException. Esta excepción se arroja debido a que el objeto Collection fue creado dentro del alcance de la transacción getReservations(), no en el alcance de la transacción del metodoX().
El objeto Collection de un bean de entidad puede usarse por otro bean co-ubicado sólo si se obtiene y se accede en el mismo contexto de transacción. En tanto que el método getReservations() del EJB Customer propage el contexto de transaccción del metodoX(), el objeto Collection se puede usar sin problemas. Esto se puede conseguir cambiando el método getReservations() para que se declare su atributo de transacción como Required o Mandatory.