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



1 comentario:

  1. Deberia usar @Transactional en metodos de servicio que solo hacen consultas a la base de datos? lo digo porque estoy usando p6spy para ver el sql enviado a la base de datos y estoy observando que para metodos marcados con @Transactional(readOnly=true) de todas formas se esta enviando un commit al final de la operacion, asi:

    p6spy - 1285083778815|0|1|statement| SELECT Capacidad_idcapacidad, nroserie, Kardex_Equipos_NroKardex, Modelos_idModelos, sbnhardware FROM accesorios_externos WHERE accesorios_externos.Kardex_Equipos_NroKardex = ? | SELECT Capacidad_idcapacidad, nroserie, Kardex_Equipos_NroKardex, Modelos_idModelos, sbnhardware FROM accesorios_externos WHERE accesorios_externos.Kardex_Equipos_NroKardex = '00000026'
    p6spy - 1285083778815|-1||resultset| SELECT Capacidad_idcapacidad, nroserie, Kardex_Equipos_NroKardex, Modelos_idModelos, sbnhardware FROM accesorios_externos WHERE accesorios_externos.Kardex_Equipos_NroKardex = '00000026' |Kardex_Equipos_NroKardex = 00000026, Modelos_idModelos = 0000000020, nroserie = un mouse, sbnhardware = mouse-sbn
    p6spy - 1285083778815|0|1|commit||

    ResponderBorrar

Es bueno comunicarnos, comenta!!.