Gestión de transacciones en aplicaciones Spring Boot

Concepto de transacciones en aplicaciones

En el desarrollo de aplicaciones, una transacción es una unidad de trabajo que garantiza que múltiples operaciones en la base de datos se ejecuten de manera atómica: o todas se completan con éxito, o todas se revierten en caso de fallo. Esto asegura la consistencia de los datos, especialmente en escenarios como transferencias bancarias, donde la actualización de múltiples registros debe ser indivisible.

En frameworks anteriores como Spring MVC o aplicaciones basadas en Java EE, la gestión de transacciones se configuraba explícitamente en capas de servicio, a menudo mediante archivos XML de configuración. Spring Boot simplifica este proceso al integrar soporte transaccional directamente en el código mediante anotaciones.

Implementación de transacciones en Spring Boot con un ejemplo

Consideremos un caso práctico: actualizar el saldo de dos cuentas de usuario de forma consistente. Usaremos Spring Data JPA para interactuar con la base de datos. A continuación, se muestra una implementación modificada con cambios en la estructura del código, nombres de variables y lógica, manteniendo la funcionalidad original.

Entidad de cuenta

package com.ejemplo.modelo;

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

@Entity
@Table(name = "cuentas")
public class Cuenta {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "nombre_usuario", length = 60)
    private String nombreUsuario;

    @Column(name = "saldo_actual")
    private double saldoActual;

    // Getters y setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNombreUsuario() {
        return nombreUsuario;
    }

    public void setNombreUsuario(String nombreUsuario) {
        this.nombreUsuario = nombreUsuario;
    }

    public double getSaldoActual() {
        return saldoActual;
    }

    public void setSaldoActual(double saldoActual) {
        this.saldoActual = saldoActual;
    }
}

Repositorio de acceso a datos

package com.ejemplo.repositorio;

import org.springframework.data.jpa.repository.JpaRepository;
import com.ejemplo.modelo.Cuenta;

public interface CuentaRepositorio extends JpaRepository<Cuenta, Long> {
}

Interfaz del servicio

package com.ejemplo.servicio;

public interface ServicioOperacionesCuentas {
    void ejecutarTransferencia(Long origenId, Long destinoId, double cantidad);
}

Implementación del servicio con gestión transaccional

package com.ejemplo.servicio.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ejemplo.modelo.Cuenta;
import com.ejemplo.repositorio.CuentaRepositorio;
import com.ejemplo.servicio.ServicioOperacionesCuentas;

@Service
public class ServicioOperacionesCuentasImpl implements ServicioOperacionesCuentas {

    private final CuentaRepositorio cuentaRepositorio;

    @Autowired
    public ServicioOperacionesCuentasImpl(CuentaRepositorio cuentaRepositorio) {
        this.cuentaRepositorio = cuentaRepositorio;
    }

    @Override
    @Transactional
    public void ejecutarTransferencia(Long origenId, Long destinoId, double cantidad) {
        Cuenta cuentaOrigen = cuentaRepositorio.findById(origenId)
            .orElseThrow(() -> new RuntimeException("Cuenta origen no encontrada"));
        cuentaOrigen.setSaldoActual(cuentaOrigen.getSaldoActual() - cantidad);
        cuentaRepositorio.save(cuentaOrigen);

        Cuenta cuentaDestino = cuentaRepositorio.findById(destinoId)
            .orElseThrow(() -> new RuntimeException("Cuenta destino no encontrada"));
        // Simular un error en el proceso
        if (cantidad < 0) {
            throw new IllegalArgumentException("La cantidad no puede ser negativa");
        }
        cuentaDestino.setSaldoActual(cuentaDestino.getSaldoActual() + cantidad);
        cuentaRepositorio.save(cuentaDestino);
    }
}

Controlador REST

package com.ejemplo.controlador;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ejemplo.servicio.ServicioOperacionesCuentas;

@RestController
@RequestMapping("/api/operaciones")
public class ControladorTransferencias {

    private final ServicioOperacionesCuentas servicioOperaciones;

    @Autowired
    public ControladorTransferencias(ServicioOperacionesCuentas servicioOperaciones) {
        this.servicioOperaciones = servicioOperaciones;
    }

    @PostMapping("/transferir")
    public String transferirFondos(@RequestParam Long desde, @RequestParam Long hacia, @RequestParam double monto) {
        try {
            servicioOperaciones.ejecutarTransferencia(desde, hacia, monto);
            return "Operación completada con éxito";
        } catch (Exception e) {
            return "Error en la operación: " + e.getMessage();
        }
    }
}

Prueba y verificación

Al ejecutar la alpicación con una base de datos MySQL, se crea la tabla cuentas con los campos definidos. Insertemos datos iniciales para la prueba:

-- Estructura de la tabla
DESCRIBE cuentas;
+----------------+--------------+------+-----+---------+----------------+
| Field          | Type         | Null | Key | Default | Extra          |
+----------------+--------------+------+-----+---------+----------------+
| id             | bigint       | NO   | PRI | NULL    | auto_increment |
| nombre_usuario | varchar(60)  | YES  |     | NULL    |                |
| saldo_actual   | double       | NO   |     | NULL    |                |
+----------------+--------------+------+-----+---------+----------------+

-- Datos de ejemplo
SELECT * FROM cuentas;
+----+----------------+--------------+
| id | nombre_usuario | saldo_actual |
+----+----------------+--------------+
|  1 | usuario_prueba |         1000 |
|  2 | usuario_secund |          500 |
+----+----------------+--------------+

Realizamos una solicitud POST a http://localhost:8080/api/operaciones/transferir?desde=1&hacia=2&monto=200. Si ocurre un error durante la transferencia, como una cantidad inválida, la transacción se revierte, y los saldos permanecen sin cambios. Esto demuestra que la anotación @Transactional garantiza la atomicidad de las operaciones.

En los registros de SQL, se observarán consultas SELECT y posibles operaciones SAVEPOINT, dependiendo del nivel de aislamiento configurado. Spring Boot maneja la transacción automáticamente, ejecutando un COMMIT solo si todas las operaciones del método son exitosas.

Se puede utilizar tanto org.springframework.transaction.annotation.Transactional como javax.transaction.Transactional, ya que Spring Boot soporta ambas. La anotación de Spring ofrece funcionalidades adicionales, como la propagación de transacciones.

Etiquetas: Spring Boot JPA Hibernate transacciones java

Publicado el 7-8 22:10