miércoles, 24 de febrero de 2010

Implementando Transacciones distribuidas con Spring y Atomikos

Una transacción distribuida debe entenderse como aquella transacción(con sus propiedades ACID) que llega a ejecutarse a través de multiples recursos y por recurso debe entenderse una base de datos, una cola de mensajes(JMS), etc..., en distintos hosts, por ejemplo:
Como aseguro que una transacción interbancaria se realice con exito, es decir retirar dinero de un banco y depositarlo en otro, teniendo en cuenta que el gestor de bases de datos del banco origen y destino son diferentes(por ejemplo mysql y postgresql) y que están en distintos hosts.

Definamos entonces de una manera más simple la transacción distribuida de ejemplo:
begin Tx.
retirar dinero del banco origen, por ejemplo MYSQL
depositar dinero al banco destino por ejemplo POSTGRESQL.
si(no excepciones)
commit Tx.
sino
rollback Tx
Una transacción distribuida asegura que se realicen todos los pasos dentro de la transacción de forma correcta o no se realice ninguno.
Por ejemplo si es que a ocurrido una excepción al depositar el dinero en el banco destino entonces la transacción distribuida debe de realizar un rollback y deshacer la acción de retiro de dinero del banco origen.

Lo anterior se realiza mediante un mecanismo llamado commit en dos fases(two-phase commit) el cual es usado para coordinar multiples recursos durante una transacción distribuida, este mecanismo como su nombre lo indica consiste en dos fases:
Fase 1: La fase de preparación.
Fase 2: La fase de compromiso(commit).

Cuando solicitamos un commit hacia una transacción distribuida el administrador transaccional inicia el mecanismo del commit en dos fases de la siguiente manera:

En la fase uno a todos los recursos involucrados en la transacción(bases de datos, colas) se les pregunta por su estado, cada recurso puede responder con cualquiera de los 3 siguientes estados: READY, READ_ONLY y NOT_READY. Si cualquiera de los recursos responde con NOT_READY entonces la transacción entera es deshecha(se realiza un rollback). Si todos los recursos responden con READY entonces estos son comprometidos(commit) en la segunda fase. Los recursos que responden con READ_ONLY no participan de la segunda fase.

En el ejemplo que vamos a ver a continuación usamos el administrador transaccional Atomikos ya que no requiere el uso de un servidor de aplicaciones J2EE, hay que recordar que la administración transaccional(sea local o distribuida) la proporciona el servidor de aplicaciones(jboss, weblogic, websphere, etc).

El ejemplo consiste en realizar una transacción interbancaria simple:

1. Retiro dinero del banco BBVA estando sus cuentas en una base de datos mysql5.0
2. El dinero obtenido del punto 1 lo deposito al banco Scotiabank en una base de datos postgres 8.4

El código fuente es el siguiente:

1. Script de base de datos tanto para mysql como para postgres:
CREATE TABLE cuenta
(
co_cuenta char(10) NOT NULL,
va_monto decimal(10,2),
PRIMARY KEY (co_cuenta)
);

insert into cuenta values('0000000001',10000);
insert into cuenta values('0000000002',10000);
insert into cuenta values('0000000003',10000);
insert into cuenta values('0000000004',10000);
insert into cuenta values('0000000005',10000);

2. Modificar el archivo de configuración del postgresql(postgresql.conf) para habilitar las transacciones distribuidas:
max_prepared_transactions = 20 # zero disables the feature

3. Crear el bean de configuración de spring(application_xa.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- 1. Inicialización del log4j: -->
<bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass" value="org.springframework.util.Log4jConfigurer" />
<property name="targetMethod" value="initLogging" />
<property name="arguments">
<list><value>C:\\logs\spring.config</value></list>
</property>
</bean>
<!-- 2. Declaración del dataSource para conexiones al BBVA -->
<bean id="dataSourceBBVA" class="com.atomikos.jdbc.AtomikosDataSourceBean">
<property name="uniqueResourceName" value="MYSQLDS"/>
<property name="xaDataSourceClassName" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
<property name="xaProperties">
<props>
<prop key="serverName">localhost</prop>
<prop key="databaseName">bancoBBVA</prop>
<prop key="user">root</prop>
<prop key="password">jdeveloper007</prop>
</props>
</property>
<property name="minPoolSize" value="2"/>
<property name="maxPoolSize" value="10"/>
</bean>
<!-- 3. Declaración del dataSource para conexiones al Scotiabank -->
<bean id="dataSourceScotiabank" class="com.atomikos.jdbc.AtomikosDataSourceBean">
<property name="uniqueResourceName" value="POSTGRESDS"/>
<property name="xaDataSourceClassName" value="org.postgresql.xa.PGXADataSource"/>
<property name="xaProperties">
<props>
<prop key="serverName">localhost</prop>
<prop key="databaseName">bancoScotiabank</prop>
<prop key="user">postgres</prop>
<prop key="password">Jdeveloper007</prop>
</props>
</property>
<property name="minPoolSize" value="2"/>
<property name="maxPoolSize" value="10"/>
</bean>
<!-- 4. Declaración del administrador transaccional que implementa todo el trabajo
transaccional, proporcionado por 'Atomikos' : -->
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init" destroy-method="close">
<property name="forceShutdown" value="true"/>
<property name="startupTransactionService" value="true"/>
</bean>

<!-- 5. Declaración del user transaction de bajo nivel -->
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp"></bean>
<!-- 6. Al administrador transaccional JTA de spring le pasamos las referencias
implementadas por 'Atomikos' y no por un servidor de aplicaciones -->
<bean id="administradorTransaccional" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="atomikosTransactionManager"/>
<property name="userTransaction" ref="atomikosUserTransaction"/>
</bean>
<!-- 7. Habilitamos la gestión de transacciones mediante anotaciones: -->
<tx:annotation-driven transaction-manager="administradorTransaccional" />
<!-- 8. Declaración del mapeo sql para la capa de base de datos con iBatis -->
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="configLocation" value="sql-map-config.xml"/>
</bean>
<!-- 9. Definición de los daos de acceso a datos: -->
<bean id="movimientoBBVADAO" class="pe.com.slcsccy.testspring.model.dao.MovimientoBBVADAOImpl">
<property name="dataSource" ref="dataSourceBBVA"/>
<property name="sqlMapClient" ref="sqlMapClient"/>
</bean>
<bean id="movimientoScotiabankDAO" class="pe.com.slcsccy.testspring.model.dao.MovimientoScotiabankDAOImpl">
<property name="dataSource" ref="dataSourceScotiabank"/>
<property name="sqlMapClient" ref="sqlMapClient"/>
</bean>
<!-- 10. Scaneo de todos los stereotipos @Service -->
<context:component-scan base-package="pe.com.slcsccy.testspring.service"/>
</beans>

4. Creamos los daos y la configuración de ibatis:
4.1 MovimientoBBVADAO.java:
package pe.com.slcsccy.testspring.model.dao;
import pe.com.slcsccy.testspring.dominio.beans.Movimiento;
public interface MovimientoBBVADAO {
public void retirarBBVA(Movimiento cuenta);
}

4.2 MovimientoBBVADAOImpl.java:
package pe.com.slcsccy.testspring.model.dao;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;
import pe.com.slcsccy.testspring.dominio.beans.Movimiento;
public class MovimientoBBVADAOImpl extends SqlMapClientDaoSupport implements MovimientoBBVADAO{
@Override
public void retirarBBVA(Movimiento movimiento) {
this.getSqlMapClientTemplate().update("CUENTAS.retirarBBVA", movimiento);
}
}

4.3 MovimientoScotiabankDAO.java:
package pe.com.slcsccy.testspring.model.dao;
import pe.com.slcsccy.testspring.dominio.beans.Movimiento;
public interface MovimientoScotiabankDAO {
public void depositarScotiabank(Movimiento movimiento)throws Exception;
}

4.4 MovimientoScotiabankDAOImpl.java:
package pe.com.slcsccy.testspring.model.dao;
import java.math.BigDecimal;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;
import pe.com.slcsccy.testspring.dominio.beans.Movimiento;
public class MovimientoScotiabankDAOImpl extends SqlMapClientDaoSupport implements MovimientoScotiabankDAO{
private static final BigDecimal vaLimiteTransferencia = BigDecimal.valueOf(2000);
@Override
public void depositarScotiabank(Movimiento movimiento) throws Exception {
if(movimiento.getVaMonto().compareTo(vaLimiteTransferencia)>0){
throw new Exception("El monto a transferir supera el límite");
}
this.getSqlMapClientTemplate().update("CUENTAS.depositarScotiabank", movimiento);
}
}

4.5 CUENTAS_SqlMap.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd" >
<sqlMap namespace="CUENTAS">
<update id="retirarBBVA" parameterClass="pe.com.slcsccy.testspring.dominio.beans.Movimiento" >
UPDATE cuenta SET va_monto = va_monto - #vaMonto#
WHERE co_cuenta = #coCuentaInterbancario#
</update>
<update id="depositarScotiabank" parameterClass="pe.com.slcsccy.testspring.dominio.beans.Movimiento" >
UPDATE cuenta SET va_monto = va_monto + #vaMonto#
WHERE co_cuenta = #coCuentaInterbancario#
</update>
</sqlMap>


5. Definimos las clases que representan al servicio:

5.1 FacadeService.java
package pe.com.slcsccy.testspring.service;
import pe.com.slcsccy.testspring.dominio.beans.Movimiento;
public interface FacadeService {
public void transferenciaInterbancaria(Movimiento movimiento) throws Exception;
}

5.2 FacadeServiceImpl.java
package pe.com.slcsccy.testspring.service;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pe.com.slcsccy.testspring.dominio.beans.Movimiento;
import pe.com.slcsccy.testspring.model.dao.MovimientoBBVADAO;
import pe.com.slcsccy.testspring.model.dao.MovimientoScotiabankDAO;

@Service("facadeService")
@Transactional
public class FacadeServiceImpl implements FacadeService {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
private MovimientoBBVADAO movimientoBBVADAO;
@Autowired
private MovimientoScotiabankDAO movimientoScotiabankDAO;
@Override
@Transactional(rollbackFor=Exception.class,readOnly=false)
public void transferenciaInterbancaria(Movimiento movimiento) throws Exception {
logger.info("INICIANDO TRANSFERENCIA...");
movimientoBBVADAO.retirarBBVA(movimiento);
movimientoScotiabankDAO.depositarScotiabank(movimiento);
logger.info("TRANSFERENCIA FINALIZADA...");
}
}

6. Por ultimo la invocación al servicio anterior
6.1 Inicio2.java:
package pe.com.slcsccy.testspring;
import java.math.BigDecimal;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pe.com.slcsccy.testspring.dominio.beans.Movimiento;
import pe.com.slcsccy.testspring.service.FacadeService;

public class Inicio2 {

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application_xa.xml");
FacadeService servicioTx = (FacadeService)context.getBean("facadeService");
Movimiento movimiento = new Movimiento();
movimiento.setCoCuentaInterbancario("0000000004");
movimiento.setVaMonto(BigDecimal.valueOf(1500));
try {
servicioTx.transferenciaInterbancaria(movimiento);
} catch (Exception e) {
e.printStackTrace();
}
}
}

Si quieres que se realice un rollback al movimiento colocale un monto mayor a 2000 y verás que hace un rollback a la base del BBVA tambien.

martes, 23 de febrero de 2010

Implementando Transacciones con Spring y Anotaciones @Transactional Parte 3

En esta oportunidad vamos a ver como podemos propagar las transacciones mediante spring. Se debe de entender por propagación de la transacción al mecanismo que extiende la transacción hacia otros métodos también transaccionales. En los ejemplos anteriores habíamos visto transacciones encapsuladas en un solo método, por ejemplo:

@Transactional
public class ClaseTransaccionalImpl implements ClaseTransaccional{
public void metodoTransaccional01(){
dao01.ejecutar();
}
public void metodoTransaccional02(){
dao02.ejecutar();
}
}

En el código anterior cada método(invocado independientemente) inicia y cierra una transacción, pero que pasa si cualquiera de los dos métodos llama al otro como a continuación:

@Transactional
public class ClaseTransaccionalImpl implements ClaseTransaccional{
public void metodoTransaccional01(){
dao01.ejecutar();
metodoTransaccional02();
}
public void metodoTransaccional02(){
dao02.ejecutar();
}
}

Ahora vemos que el método metodoTransaccional01() invoca al método metodoTransaccional02(), pero que es lo que pasa con la transacción iniciada por la invocación de metodoTransaccional01()?.
se realiza un commit antes de la ejecución del siguiente método?
la transacción se suspende y se crea otra transacción por la invocación del nuevo método?
la transacción se extiende(PROPAGA) y la ejecución de los dos métodos se ejecutan en una sola transacción?
Recordando del post anterior los atributos transaccionales por default a nivel de método son los siguientes:
propagation: REQUIRED.
isolation: DEFAULT(READ_COMMITED para Oracle)
timeout: -1
readOnly: false

Entonces la clase anterior definida explicitamente quedaría como:
@Transactional
public class ClaseTransaccionalImpl implements ClaseTransaccional{

@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.READ_COMMITTED,readOnly=false)
public void metodoTransaccional01(){
dao01.ejecutar();
metodoTransaccional02();
}

@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.READ_COMMITTED,readOnly=false)
public void metodoTransaccional02(){
dao02.ejecutar();
}
}

Entonces que se puede deducir?, pues que la transacción se propaga hacia el método metodoTransaccional02(), ya que el atributo de propagación de la transacción es REQUIRED. Veamoslo más despacio:

1. Supongamos que desde una clase cualquiera ejecutamos lo siguiente:
ClaseTransaccional servicio = (ClaseTransaccional)context.getBean("claseTransaccional");
servicio.metodoTransaccional01();

2. Lo que hará spring es detectar que el método a sido marcado como transaccional e iniciará una transacción.

3. La ejecución del método se realiza e invoca al DAO: dao01.ejecutar();

4. La ejecución del método continua y encuentra el método metodoTransaccional02() antes de ejecutar este método spring identifica que es transaccional y que su tipo de propagación es REQUIRED, es decir que si ya existe un contexto transaccional entonces comparte dicho contexto, y si es que no existe el contexto transaccional entonces debe de crear una nueva transacción, para el ejemplo el método transaccional ha sido invocado en un contexto transaccional definido por metodoTransaccional01() por lo que la transacción se propaga a este nuevo método.

5. Si el método metodoTransaccional02() termina exitosamente, es decir no lanza alguna excepción entonces el método metodoTransaccional01() continua su ejecucion y si termina satisfactoriamente realiza un commit de la transacción, dando la posta al hilo que lo invocó.
6. Si en el punto 5 al metodo metodoTransaccional01() le hemos aplicado un aspecto afterReturning entonces la transacción se propagará a dicho aspecto.

domingo, 7 de febrero de 2010

Implementando Transacciones con Spring y Anotaciones @Transactional Parte 2

En el siguiente post vamos a ver el manejo de los atributos transaccionales en spring, a modo de teoria recordemos dichos atributos:

isolation: El grado de aislamiento que tiene esta transacción para otras transacciones, Por ejemplo, puede esta transacción ver registros no comprometidos(uncommited) desde otras transacciones?
propagation: Normalmente todo el código ejecutado dentro del alcance de una transacción será ejecutado en esa transacción. Como vemos, existen varias opciones que especifican el comportamiento si un método es ejecutado cuando un contexto transaccional ya existe. Por ejemplo, si un contexto transaccional ya existe podemos continuar ejecutando el método en la transacción existente o podemos suspender la transacción y crear una nueva transacción. Spring ofrece todas las opciones de propagación que ofrece EJB. Dichas opciones de propagación son:

  • Propagation.REQUIRED: Si el método es invocado desde un contexto transaccional, entonces el método será invocado en el mismo contexto transaccional. Si el método no es invocado desde un contexto transaccional, entonces el método creará una nueva transacción e intentará comprometer(commit) la transacción cuando el método termine su ejecución.
  • Propagation.REQUIRES_NEW: El método siempre creará una nueva transacción cuando sea invocado y comprometerá(commit) la transacción cuando el método termine su ejecución. Si ya existe un contexto transaccional, entonces spring suspenderá la transacción existente y creará otra transacción, cuando el método termine su ejecución comprometerá la transacción y reanudará la transacción suspendida.
  • Propagation.NOT_SUPPORTED: Si el método es ejecutado en un contexto transaccional, entonces este contexto no es propagado a la ejecución del método, por lo que spring suspenderá el contexto transaccional y lo reanudará cuando el método termine su ejecución.
  • Propagation.SUPPORTS: Si ya existe un contexto transaccional, entonces el método será invocado en el mismo contexto transaccional(igual que REQUIRED), si no existe un contexto transaccional entonces no se crea un contexto transaccional (igual que NOT_SUPPORTED)
  • Propagation.MANDATORY: Este atributo obliga a la transaccion a ser ejecutada en un contexto transaccional, si es que no existe un contexto transaccional en la ejecución del método spring retorna una Excepción.
  • Propagation.NEVER: Este atributo obliga que la ejecución del método no sea invocado desde un contexto transaccional, de lo contrario spring retorna una excepcion.
  • Propagation.NESTED: (Solo en spring no en EJB), Se ejecuta dentro de una transacción anidada si un contexto transaccional existe.

timeout: Cuanto tiempo esta transacción puede ejecutarse antes de que automáticamente se realice un rollback.
readOnly: Una transacción de solo lectura no modifica datos. Transacciones de solo lectura pueden ser usuales en algunos casos de optimización(por ejemplo cuando usas Hibernate).

En el siguiente ejemplo mediante spring y oracle vamos a probar los 2 niveles de aislamiento(isolation) que Oracle ofrece, el primero es READ_COMMITED(por default) y el otro es el SERIALIZABLE, resumiendo:

READ_COMMITED: Nivel de aislamiento en el que una transacción solo puede ver data que a sido comprometida(commit) en la base datos.
SERIALIZABLE: Este nivel de aislamiento es generalmente considerado el más restrictivo, ya que proporciona el más alto nivel de aislamiento transaccional. Una transacción SERIALIZABLE opera en un entorno donde parece que no hubieran otros usuarios modificando la data en la base de datos. Cualquier fila leída es asegurada a ser la misma así sea releída y cualquier consulta que ejecutemos es garantizada a retornar los mismos resultados durante la vida de la transacción.

Veamos un par de ejemplos:

El gestor de servicios FacadeServiceImpl.java ahora le he agregado 2 métodos más, un método que soporta las transacciones con un nivel de aislamiento SERIALIZABLE y otra READ_COMMITED, para probar el aislamiento SERIALIZABLE el método buscarEmpleado busca en la base un empleado con código X que al inicio de la transacción no existe, entonces duermo el hilo en ejecución por 10 segundos(Thread.sleep(1000*10);) y durante esos 10 segundos con el usuario sys(en sqlplus) ingreso un empleado con código X y comprometo(commit) la transacción, después que terminan los 10 segundos el programa intenta denuevo buscar el empleado pero tampoco lo encuentra a pesar del commit, este nivel de aislamiento es el más restrictivo:SERIALIZABLE, a diferencia del READ_COMMITED, este último si logra leer los datos comprometidos luego de pasados los 10 segundos:

FacadeService.java
package pe.com.slcsccy.testspring.service;
import java.util.List;
import pe.com.slcsccy.testspring.dominio.beans.Employees;
public interface FacadeService {
public void registrarEmpleados(List empleados);
public void testNivelAislamientoSerializable(Integer idEmpleado);
public void testNivelAislamientoReadCommited(Integer idEmpleado);
}

FacadeServiceImpl.java
package pe.com.slcsccy.testspring.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import pe.com.slcsccy.testspring.dominio.beans.Employees;
import pe.com.slcsccy.testspring.model.dao.EmployeesDAO;

@Service("facadeService")
@Transactional
public class FacadeServiceImpl implements FacadeService {
@Autowired
private EmployeesDAO empleadoDAO;
@Override
public void registrarEmpleados(List empleados) {
for(Employees e:empleados){
empleadoDAO.insert(e);
}
}

@Override
@Transactional(isolation=Isolation.SERIALIZABLE)
public void testNivelAislamientoSerializable(Integer idEmpleado) {
buscarEmpleado(idEmpleado);
}

@Override
@Transactional(isolation=Isolation.READ_COMMITTED)
public void testNivelAislamientoReadCommited(Integer idEmpleado) {
buscarEmpleado(idEmpleado);
}
private void buscarEmpleado(Integer idEmpleado){
Employees empleado = empleadoDAO.selectByPrimaryKey(idEmpleado);
if(empleado!=null)System.out.println("Empleado 1:"+empleado.getLastName());
else System.out.println("Empleado NO Existe.");
//Duermo el programa para insertar un
//empleado con código idEmpleado y pueda ser leído posteriormente.
//esto lo realizo con el sqlplus:
//SYS: OPTIMUS> insert into //hr.employees(employee_id,last_name,email,hire_date,job_id)
//values(20002,'Carlitos','algo3@algo.com',sysdate,'IT_PROG');
//1 fila creada.
//SYS: OPTIMUS> commit;
//Confirmación terminada.

try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
empleado = empleadoDAO.selectByPrimaryKey(idEmpleado);
if(empleado!=null)System.out.println("Empleado 2:"+empleado.getLastName());
else System.out.println("Empleado NO Existe.");
}
}


Por ultimo les muestro el log de ejecución para el nivel de aislamiento SERIALIZABLE, vean como a pesar de que creo el registro y lo comprometo(en sqlplus durante los 10 segundos), en la segunda lectura(después de los 10 segundos) Oracle aún no reconoce el registro para esta transacción, ya que esta transacción es completamente aislada de las demás:

2010-02-07 23:13:33 DEBUG [main] (AbstractFallbackTransactionAttributeSource.java:107) - Adding transactional method [testNivelAislamientoSerializable] with attribute [PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE]
2010-02-07 23:13:33 DEBUG [main] (AbstractPlatformTransactionManager.java:371) - Creating new transaction with name [pe.com.slcsccy.testspring.service.FacadeService.testNivelAislamientoSerializable]: PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE
2010-02-07 23:13:34 DEBUG [main] (DataSourceTransactionManager.java:202) - Acquired Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] for JDBC transaction
2010-02-07 23:13:34 DEBUG [main] (DataSourceUtils.java:170) - Changing isolation level of JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] to 8
2010-02-07 23:13:34 DEBUG [main] (DataSourceTransactionManager.java:219) - Switching JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] to manual commit
2010-02-07 23:13:34 DEBUG [main] (SqlMapClientTemplate.java:177) - Opened SqlMapSession [com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl@1786286] for iBATIS operation
2010-02-07 23:13:34 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100000} Connection
2010-02-07 23:13:34 DEBUG [main] (SqlMapClientTemplate.java:194) - Obtained JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] for iBATIS operation
2010-02-07 23:13:34 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100000} Preparing Statement: select EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID from EMPLOYEES where EMPLOYEE_ID = ?
2010-02-07 23:13:34 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100001} Executing Statement: select EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID from EMPLOYEES where EMPLOYEE_ID = ?
2010-02-07 23:13:34 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100001} Parameters: [20002]
2010-02-07 23:13:34 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100001} Types: [java.lang.Integer]
2010-02-07 23:13:34 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {rset-100002} ResultSet
Empleado NO Existe.
2010-02-07 23:13:44 DEBUG [main] (SqlMapClientTemplate.java:177) - Opened SqlMapSession [com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl@c792d4] for iBATIS operation
2010-02-07 23:13:44 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100003} Connection
2010-02-07 23:13:44 DEBUG [main] (SqlMapClientTemplate.java:194) - Obtained JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] for iBATIS operation
2010-02-07 23:13:44 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100003} Preparing Statement: select EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID from EMPLOYEES where EMPLOYEE_ID = ?
2010-02-07 23:13:44 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100004} Executing Statement: select EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID from EMPLOYEES where EMPLOYEE_ID = ?
2010-02-07 23:13:44 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100004} Parameters: [20002]
2010-02-07 23:13:44 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100004} Types: [java.lang.Integer]
2010-02-07 23:13:44 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {rset-100005} ResultSet
Empleado NO Existe.
2010-02-07 23:13:44 DEBUG [main] (AbstractPlatformTransactionManager.java:730) - Initiating transaction commit
2010-02-07 23:13:44 DEBUG [main] (DataSourceTransactionManager.java:259) - Committing JDBC transaction on Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver]
2010-02-07 23:13:44 DEBUG [main] (DataSourceUtils.java:193) - Resetting isolation level of JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] to 2
2010-02-07 23:13:44 DEBUG [main] (DataSourceTransactionManager.java:314) - Releasing JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] after transaction
2010-02-07 23:13:44 DEBUG [main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource



Implementando Transacciones con Spring y Anotaciones @Transactional Parte 1

Este post va a ser el primero de una serie el cuál estará enfocado a la implementación de transacciones mediante spring, estas implementaciones las realizaré mediantes anotaciones, con la anotación @Transactional, y en forma declarativa mediante xml y aspectos.
El proyecto desarrollado hace uso del esquema de ejemplo HR que viene con Oracle 10G, la capa de acceso a datos la he implementado con ibatis y el generador de clases de acceso a datos y beans a sido abator, el objetivo del proyecto es ver como spring implementa métodos transaccionales mediante anotaciones, en este primer post vamos a ver de forma simple como antes de la ejecución de un método java spring inicia una nueva transacción y como al finalizar exitosamente dicho método realiza el commit correspondiente para persistir los objetos en forma permanente en la base de datos oracle, es decir y para recordar oracle copia los datos a sus redologs y de allí cuando crea conveniente a sus datafiles, el proyecto tambien hace uso del log4j para poder visualizar la clase de spring que crea la transacción y la clase que la compromete(commit).
Los pasos para el desarrollo del proyecto han sido los siguientes:

1. Generación de daos y beans mediante abator(ahora ibator) , el archivo de configuración es el siguiente:
configuradorhr.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE abatorConfiguration
PUBLIC "-//Apache Software Foundation//DTD Abator for iBATIS Configuration 1.0//EN"
"http://ibatis.apache.org/dtd/abator-config_1_0.dtd">

<abatorConfiguration>
<abatorContext id="DB2Tables" generatorSet="Java5">
<jdbcConnection driverClass="oracle.jdbc.driver.OracleDriver" connectionURL="jdbc:oracle:thin:@192.168.1.11:1521:optimus"
userId="hr" password="jdeveloper">
<classPathEntry location="ojdbc6.jar" />
</jdbcConnection>

<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>

<javaModelGenerator targetPackage="pe.com.slcsccy.testspring.dominio.beans" targetProject=".">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>

<sqlMapGenerator targetPackage="pe.com.slcsccy.testspring.model.dao.ibatis" targetProject=".">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>

<daoGenerator type="SPRING" targetPackage="pe.com.slcsccy.testspring.model.dao" targetProject=".">
<property name="enableSubPackages" value="true" />
</daoGenerator>

<table tableName="countries"/>
<table tableName="departments"/>
<table tableName="employees"/>
<table tableName="job_history"/>
<table tableName="jobs"/>
<table tableName="locations"/>
<table tableName="regions"/>

</abatorContext>
</abatorConfiguration>

Ahora lo único que necesitamos para crear los beans y daos es ejecutar lo siguiente:
java -jar abator.jar configuradorhr.xml true
Debes recordar también que debes de tener el driver del oracle en tu classpath para que abator te genere todas las clases.

2. El segundo paso a sido desarrollar la capa de servicios, el servicio lo he implementado mediante una interfaz y una clase con un solo método, que es el siguiente:

FacadeService.java
package pe.com.slcsccy.testspring.service;
import java.util.List;
import pe.com.slcsccy.testspring.dominio.beans.Employees;
public interface FacadeService {
public void registrarEmpleados(List empleados);
}

FacadeServiceImpl.java
package pe.com.slcsccy.testspring.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pe.com.slcsccy.testspring.dominio.beans.Employees;
import pe.com.slcsccy.testspring.model.dao.EmployeesDAO;

@Service("facadeService")
@Transactional
public class FacadeServiceImpl implements FacadeService {
@Autowired
private EmployeesDAO empleadoDAO;
@Override
public void registrarEmpleados(List empleados) {
for(Employees e:empleados){
empleadoDAO.insert(e);
}
}
}

La clase FacadeServiceImpl.java primero la anotamos como un servicio estereotipado(@Service), es decir spring cuando encuentre esta anotación cargará el bean en memoria, la otra anotación @Transactional es la más importante para este post y nos indica que todos los métodos definidos en esta clase van a ser transaccionales, si la anotación @Transactional no le agregamos sus atributos, entonces los atributos que adquiere por defecto son los siguientes:

propagation: REQUIRED, es la conducta de propagación de la transacción, esto quiere decir que si el método no se ejecuta en un contexto transaccional, entonces spring crea una nueva transacción y si ya está en un contexto transaccional(por algún otro lado se inició una transacción), entonces el método se acopla a la transacción.
isolation: DEFAULT, el nivel de aislamiento de la transacción.
timeout: -1, el tiempo máximo en segundos que debe de ejecutarse la transacción.
read-only: false, es la transacción de solo lectura?.

Algunos definiciones personalizadas de @Transactional podrían ser la siguientes:
@Transactional(readOnly=false, propagation=Propagation.REQUIRES_NEW)
Esta definición a nivel de clase nos indica que todos los métodos no son de solo lectura(pueden hacer update insert a la base), y su nivel de propagación requiere una nueva transacción, es decir cuando la aplicación realice una invocación a un método de esta clase se deberá crear una nueva transacción.

@Transactional(readOnly=true)
Esta definición a nivel de clase nos indica que todos los métodos son de solo lectura, es decir nuestros métodos no puede realizar insert o update, si lo hacen entonces spring lanza una excepción.

3. El ultimo paso es la integración de los componentes en un contenedor de beans de spring:
application.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- Inicialización del log4j: -->
<bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass" value="org.springframework.util.Log4jConfigurer" />
<property name="targetMethod" value="initLogging" />
<property name="arguments">
<list><value>C:\\logs\spring.config</value></list>
</property>
</bean>
<!-- Definición del data source o fuente de datos Oracle -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
p:driverClassName="oracle.jdbc.driver.OracleDriver"
p:url="jdbc:oracle:thin:@192.168.1.11:1521:optimus"
p:username="hr" p:password="jdeveloper"/>
<!-- Administrador transaccional para los objetos de acceso a datos con iBatis : -->
<bean id="administradorTransaccional" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<!-- Gestor de transacciones mediante anotaciones: -->
<tx:annotation-driven transaction-manager="administradorTransaccional" />
<!-- Declaración del mapeo sql para la capa de base de datos con iBatis -->
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"
p:configLocation="sql-map-config.xml"/>
<!-- Definiciòn de los daos de acceso a datos: -->
<bean id="abstractDao" abstract="true" p:sqlMapClient-ref="sqlMapClient"
p:dataSource-ref="dataSource"/>
<bean id="countriesDAO" class="pe.com.slcsccy.testspring.model.dao.CountriesDAOImpl" parent="abstractDao"/>
<bean id="departmentsDAO" class="pe.com.slcsccy.testspring.model.dao.DepartmentsDAOImpl" parent="abstractDao"/>
<bean id="employeesDAO" class="pe.com.slcsccy.testspring.model.dao.EmployeesDAOImpl" parent="abstractDao"/>
<bean id="jobHistoryDAO" class="pe.com.slcsccy.testspring.model.dao.JobHistoryDAOImpl" parent="abstractDao"/>
<bean id="jobsDAO" class="pe.com.slcsccy.testspring.model.dao.JobsDAOImpl" parent="abstractDao"/>
<bean id="locationsDAO" class="pe.com.slcsccy.testspring.model.dao.LocationsDAOImpl" parent="abstractDao"/>
<bean id="regionsDAO" class="pe.com.slcsccy.testspring.model.dao.RegionsDAOImpl" parent="abstractDao"/>
<!-- Scaneo de todos los stereotipos @Service -->
<context:component-scan base-package="pe.com.slcsccy.testspring.service"/>
</beans>

La ejecución del servicio que registra 3 empleados es la siguiente:

Inicio.java
package pe.com.slcsccy.testspring;
import java.util.ArrayList;
import java.util.Date;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pe.com.slcsccy.testspring.dominio.beans.Employees;
import pe.com.slcsccy.testspring.service.FacadeService;
public class Inicio {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
FacadeService servicioTx = (FacadeService)context.getBean("facadeService");
ArrayList empleados = new ArrayList() ;
//Empleado 1.
Employees empleado1 = new Employees();
empleado1.setEmployeeId(100001);
empleado1.setLastName("ccacique1");
empleado1.setEmail("carlos.cacique1@gmail.com");
empleado1.setHireDate(new Date(System.nanoTime()));
empleado1.setJobId("IT_PROG");
//Empleado 2.
Employees empleado2 = new Employees();
empleado2.setEmployeeId(100002);
empleado2.setLastName("ccacique2");
empleado2.setEmail("carlos.cacique2@gmail.com");
empleado2.setHireDate(new Date(System.nanoTime()));
empleado2.setJobId("IT_PROG");
//Empleado 3.
Employees empleado3 = new Employees();
empleado3.setEmployeeId(100003);
empleado3.setLastName("ccacique3");
empleado3.setEmail("carlos.cacique3@gmail.com");
empleado3.setHireDate(new Date(System.nanoTime()));
empleado3.setJobId("IT_PROG");
//Colocamos a todos los empleados en una lista:
empleados.add(empleado1);
empleados.add(empleado2);
empleados.add(empleado3);
//Ejecutamos el servicio transaccional.
servicioTx.registrarEmpleados(empleados);
}
}

El log generado es el siguiente, vean como spring crea y compromete(commit) la transacción:
2010-02-07 11:28:10 DEBUG [main] (AbstractFallbackTransactionAttributeSource.java:107) - Adding transactional method [registrarEmpleados] with attribute [PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
2010-02-07 11:28:10 DEBUG [main] (AbstractPlatformTransactionManager.java:371) - Creating new transaction with name [pe.com.slcsccy.testspring.service.FacadeService.registrarEmpleados]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2010-02-07 11:28:11 DEBUG [main] (DataSourceTransactionManager.java:202) - Acquired Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] for JDBC transaction
2010-02-07 11:28:11 DEBUG [main] (DataSourceTransactionManager.java:219) - Switching JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] to manual commit
2010-02-07 11:28:11 DEBUG [main] (SqlMapClientTemplate.java:177) - Opened SqlMapSession [com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl@11c0d60] for iBATIS operation
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100000} Connection
2010-02-07 11:28:11 DEBUG [main] (SqlMapClientTemplate.java:194) - Obtained JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] for iBATIS operation
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100000} Preparing Statement: insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100001} Executing Statement: insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100001} Parameters: [100001, null, ccacique1, carlos.cacique1@gmail.com, null, 2293-08-14 05:29:50.376, IT_PROG, null, null, null, null]
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100001} Types: [java.lang.Integer, null, java.lang.String, java.lang.String, null, java.sql.Timestamp, java.lang.String, null, null, null, null]
2010-02-07 11:28:11 DEBUG [main] (SqlMapClientTemplate.java:177) - Opened SqlMapSession [com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl@e4bb3c] for iBATIS operation
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100002} Connection
2010-02-07 11:28:11 DEBUG [main] (SqlMapClientTemplate.java:194) - Obtained JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] for iBATIS operation
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100002} Preparing Statement: insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100003} Executing Statement: insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100003} Parameters: [100002, null, ccacique2, carlos.cacique2@gmail.com, null, 2293-08-14 05:30:04.039, IT_PROG, null, null, null, null]
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100003} Types: [java.lang.Integer, null, java.lang.String, java.lang.String, null, java.sql.Timestamp, java.lang.String, null, null, null, null]
2010-02-07 11:28:11 DEBUG [main] (SqlMapClientTemplate.java:177) - Opened SqlMapSession [com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl@cffc79] for iBATIS operation
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100004} Connection
2010-02-07 11:28:11 DEBUG [main] (SqlMapClientTemplate.java:194) - Obtained JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] for iBATIS operation
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {conn-100004} Preparing Statement: insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100005} Executing Statement: insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100005} Parameters: [100003, null, ccacique3, carlos.cacique3@gmail.com, null, 2293-08-14 05:30:08.251, IT_PROG, null, null, null, null]
2010-02-07 11:28:11 DEBUG [main] (JakartaCommonsLoggingImpl.java:27) - {pstm-100005} Types: [java.lang.Integer, null, java.lang.String, java.lang.String, null, java.sql.Timestamp, java.lang.String, null, null, null, null]
2010-02-07 11:28:11 DEBUG [main] (AbstractPlatformTransactionManager.java:730) - Initiating transaction commit
2010-02-07 11:28:11 DEBUG [main] (DataSourceTransactionManager.java:259) - Committing JDBC transaction on Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver]
2010-02-07 11:28:11 DEBUG [main] (DataSourceTransactionManager.java:314) - Releasing JDBC Connection [jdbc:oracle:thin:@192.168.1.11:1521:optimus, UserName=HR, Oracle JDBC driver] after transaction
2010-02-07 11:28:11 DEBUG [main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

El proyecto completo te lo puedes bajar desde AQUI