Gestión Declarativa de Transacciones en Spring con Anotaciones

La gestión de transacciones es fundamental en el desarrollo de aplicaciones empresariales para garantizar la integridad de los datos. Spring Framework ofrece un enfoque declarativo para manejar transacciones, simplificando el código de negocio y separándolo de la lógica transaccional.

¿Dónde aplicar las anotaciones de transacción: DAO o Service?

Generalmente, las transacciones se aplican en la capa de Service. Esto se debe a que un método de servicio a menudo coordina múltiples operaciones de acceso a datos (DAO). Si colocáramos la anotación transaccional en los métodos DAO individuales, cada operación se ejecutaría en su propia transacción. Si una operación falla, las otras que ya se completaron y confirmaron permanecerían así, violando la atomicidad deseada. Al colocar la anotación en el método de servicio, se asegura que todas las operaciones de DAO dentro de ese método se ejecuten como una única unidad atómica. Si alguna operación falla, toda la transacción se revierte.

Escenario de Ejemplo: Guardar Usuario y Registrar Log

Consideremos un requisito donde al guardar un nuevo usuario, también se debe registrar un mensaje de log indicando esta acción. Ambas operaciones deben ser atómicas: o se guardan el usuario y el log exitosamnete, o ninguna de las dos se realiza.

Implementación del Código

A continuación, se muestra la implementación utilizando Spring para gestionar transacciones con Hibernate.

1. Implementación del DAO para Usuarios (UserDAOImpl.java)


package com.cy.dao.impl;

import javax.annotation.Resource;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository; // Usar @Repository para DAOs

import com.cy.dao.UserDAO;
import com.cy.model.User;

@Repository("userDAO") // Anotación específica para DAOs
public class UserDAOImpl implements UserDAO {

    @Resource
    private SessionFactory sessionFactory;

    @Override
    public void save(User user) {
        Session session = sessionFactory.getCurrentSession();
        session.save(user);
    }
}

2. Implementación del DAO para Logs (LogDAOImpl.java)


package com.cy.dao.impl;

import javax.annotation.Resource;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;

import com.cy.dao.LogDAO;
import com.cy.model.Log;

@Repository("logDAO")
public class LogDAOImpl implements LogDAO {

    @Resource
    private SessionFactory sessionFactory;

    @Override
    public void save(Log log) {
        Session session = sessionFactory.getCurrentSession();
        session.save(log);
        // Simulamos un error para probar el rollback
        throw new RuntimeException("Simulación de error al guardar log.");
    }
}

3. Entidades de Modelo (User.java y Log.java)

User.java


package com.cy.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
    private Integer id;
    private String username;
    private String password;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // Estrategia de generación
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

Log.java


package com.cy.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="t_log") // Nombre de tabla personalizado
public class Log {
    private Integer id;
    private String message; // Renombrado para mayor claridad

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

4. Servicio de Usuario con Gestión Transaccional (UserService.java)

La anotación @Transactional en el método add indica que esta operación debe ejecutarse dentro de una transacción. Por defecto, Spring revertirá la transacción si se lanza una RuntimeException.


package com.cy.service;

import javax.annotation.Resource;

import org.springframework.stereotype.Service; // Anotación específica para servicios
import org.springframework.transaction.annotation.Transactional;

import com.cy.dao.LogDAO;
import com.cy.dao.UserDAO;
import com.cy.model.Log;
import com.cy.model.User;

@Service("userService")
public class UserService {

    @Resource
    private UserDAO userDAO;

    @Resource
    private LogDAO logDAO;

    // Método para inicialización (ejemplo)
    public void initialize() {
        System.out.println("Servicio de usuario inicializado.");
    }

    /**
     * Guarda un usuario y registra un log.
     * La anotación @Transactional asegura que ambas operaciones sean atómicas.
     * Por defecto, cualquier RuntimeException provocará un rollback.
     */
    @Transactional
    public void addUserAndLog(User user) {
        userDAO.save(user); // Guarda el usuario

        Log logEntry = new Log();
        logEntry.setMessage("Usuario agregado exitosamente.");
        logDAO.save(logEntry); // Intenta guardar el log (aquí se simula el error)
    }

    // Getters y Setters (necesarios si se usa inyección por tipo o setters)
    public UserDAO getUserDAO() {
        return userDAO;
    }

    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

    public LogDAO getLogDAO() {
        return logDAO;
    }

    public void setLogDAO(LogDAO logDAO) {
        this.logDAO = logDAO;
    }

    // Método para destrucción (ejemplo)
    public void destroy() {
        System.out.println("Servicio de usuario destruido.");
    }
}

5. Configuración de Spring (beans.xml)

El archivo de configuración define los beans, habilita el escaneo de componentes y configura la gestión de transacciones y la factoría de sesiones de Hibernate.


<?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- Habilita la detección de anotaciones como @Component, @Service, @Repository, @Autowired -->
    <context:annotation-config />
    <context:component-scan base-package="com.cy"/>

    <!-- Configuración de propiedades (ej. para la conexión a la BD) -->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:jdbc.properties</value>
            </list>
        </property>
    </bean>

    <!-- Bean para el DataSource -->
    <bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- Configuración de Hibernate SessionFactory -->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan"> <!-- Escanea paquetes para encontrar entidades -->
            <list>
                <value>com.cy.model</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop> <!-- Formateo SQL para legibilidad -->
            </props>
        </property>
    </bean>

    <!-- Bean para el gestor de transacciones de Hibernate -->
    <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
          <property name="sessionFactory" ref="sessionFactory" />
    </bean>

    <!-- Habilita el manejo de transacciones basado en anotaciones (@Transactional) -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

6. Código de Prueba Unitaria

Esta prueba invoca el método addUserAndLog del servicio. Dado que el método LogDAOImpl.save lanza una excepción, se espera que la transacción completa se revierta, y ni el usuario ni el log se guarden en la base de datos.


package com.cy.service;

import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.cy.model.User;

public class UserServiceTest {

    @Test
    public void testAddUserAndLog_Rollback() {
        System.out.println("Iniciando prueba de UserService...");
        // Cargar el contexto de Spring
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");

        // Obtener el bean del servicio
        UserService userService = (UserService) applicationContext.getBean("userService");
        System.out.println("Bean UserService obtenido: " + userService.getClass().getName());

        // Crear un nuevo usuario
        User newUser = new User();
        newUser.setUsername("TestUser");
        newUser.setPassword("hashedPassword");

        try {
            // Invocar el método que debería ejecutarse dentro de una transacción
            userService.addUserAndLog(newUser);
            System.out.println("Método addUserAndLog ejecutado sin excepción aparente (esto no debería ocurrir).");
        } catch (RuntimeException e) {
            // Capturar la excepción esperada que causa el rollback
            System.err.println("Excepción capturada durante la ejecución de addUserAndLog: " + e.getMessage());
            System.out.println("Se espera que la transacción se haya revertido correctamente.");
        } catch (Exception e) {
            System.err.println("Ocurrió una excepción inesperada: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // Destruir el contexto de Spring
            applicationContext.close(); // Usar close() en lugar de destroy() para ClassPathXmlApplicationContext
            System.out.println("Prueba de UserService finalizada.");
        }
    }
}

Resultado Esperado de la Prueba:

La salida mostrará que se captura la RuntimeException lanzada desde LogDAOImpl.save. Debido a la anotación @Transactional en UserService.addUserAndLog, Spring detectará esta excepción y revertirá la transacción. Por lo tanto, ni el usuario ni el registro de log se guardarán en la base de datos.

Configuración de @Transactional

La anotación @Transactional ofrece varias directivas para personalizar el comportamiento transaccional:

  • propagation: Define cómo se propaga la transacción a métodos anidados. Los valores comunes incluyen:
    • REQUIRED (predeterminado): Usa la transacción existente si hay una; de lo contrario, crea una nueva.
    • MANDATORY: Requiere una transacción existente; lanza una excepción si no la hay.
    • REQUIRES_NEW: Siempre crea una nueva transacción, suspendiendo la existente si la hay.
    • SUPPORTS: Usa la transacción existente si la hay; de lo contrario, ejecuta sin transacción.
    • NOT_SUPPORTED: Ejecuta sin transacción, suspendiendo la transacción existente si la hay.
    • NEVER: Ejecuta sin transacción; lanza una excepción si hay una.
    • NESTED: Ejecuta dentro de una transacción anidada; si la interna falla, se revierte solo ella; si la externa falla, se revierte todo.
  • isolation: Especifica el nivel de aislamiento de la transacción (ej. READ_COMMITTED, SERIALIZABLE).
  • readOnly: Si se establece en true, indica que la transacción solo realizará operaciones de lectura. Esto puede optimizar el rendimiento y prevenir modificaciones accidentales. Intentar escribir en una transacción readOnly resultará en un error (ej. java.sql.SQLException: Connection is read-only).
  • timeout: Define el tiempo máximo (en segundos) que la transacción puede durar antes de ser terminada automáticamente.
  • rollbackFor: Especifica qué tipos de excepciones deben provocar una reversión de la transacción. Por defecto, solo las RuntimeException causan rollback. Se puede usar para incluir excepciones específicas lanzadas por el código de negocio.
  • noRollbackFor: Especifica qué tipos de excepciones no deben provocar una reversión, incluso si son RuntimeException.

Diagrama de Prpoagación REQUIRED:

Cuando method1, que tiene una transacción activa, llama a method2 anotado con @Transactional(propagation = Propagation.REQUIRED), method2 simplemente se une a la transacción existente de method1. No se crea una nueva transacción.

Etiquetas: Spring transacciones Hibernate anotaciones Gestión de Transacciones

Publicado el 6-16 22:22