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.