Imagina la situación: necesitas implementar un mecanismo para transferir dinero entre dos cuentas bancarias. Obviamente, la operación consta de dos pasos:
- Debitar fondos de una cuenta.
- Acreditar fondos en la otra cuenta.
Si en el proceso algo sale mal, por ejemplo, una falla en el segundo paso, es importante revertir toda la operación para evitar que el dinero "desaparezca". Aquí entra en juego la anotación @Transactional, permitiéndote centrarte en la lógica de negocio, mientras que Spring se encarga del manejo de la transacción, dejando el código más limpio y simple.
¿Cómo funciona @Transactional?
@Transactional — es una anotación que define el comportamiento de la transacción para un método o clase. Spring usa programación orientada a aspectos (AOP) para envolver tu código en "proxies transaccionales". Esto significa que cuando se ejecuta el método, Spring automáticamente inicia una transacción, y al finalizar el método — ya sea confirma los cambios (commit) o los revierte (rollback) si ocurrió una excepción.
Aquí tienes un ejemplo de uso de @Transactional para la operación de transferencia entre cuentas:
@Service
public class BankService {
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
// Paso 1: Debitar fondos de una cuenta
accountRepository.debit(fromAccountId, amount);
// Paso 2: Acreditar fondos en la otra cuenta
accountRepository.credit(toAccountId, amount);
}
}
¿Sencillo? Pero esto es solo la punta del iceberg.
Opciones de la anotación @Transactional
@Transactional ofrece muchas opciones para controlar el comportamiento de las transacciones. Vamos a ver las más importantes.
1. Propagation (propagación)
Esto indica cómo debe ejecutarse la transacción si el método es llamado dentro de otra transacción. Por ejemplo:
REQUIRED(por defecto): Usa la transacción existente si la hay. Si no, crea una nueva.REQUIRES_NEW: siempre crea una nueva transacción, pausando la actual.MANDATORY: requiere una transacción existente. Si no la hay – lanzará una excepción.SUPPORTS: funciona dentro de una transacción si existe, pero puede invocarse sin ella.
Ejemplo de uso:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
auditLogRepository.save(log);
}
2. Isolation (nivel de aislamiento)
Indica cómo la transacción debe ver los cambios hechos por otras transacciones. Esto es importante en sistemas con concurrencia de usuarios.
Aquí están los niveles principales de aislamiento:
READ_UNCOMMITTED: la transacción puede ver cambios no confirmados de otras transacciones (dirty read).READ_COMMITTED: evita dirty reads.REPEATABLE_READ: protección contra lecturas no repetibles (consistent reads).SERIALIZABLE: aislamiento completo, máxima protección contra conflictos, pero con pérdida de rendimiento.
Ejemplo:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void performSensitiveOperation() {
// Operación críticamente importante
}
3. Timeout (tiempo de ejecución)
A veces las transacciones pueden bloquearse o durar demasiado. Puedes establecer un límite de tiempo (en segundos), tras el cual la transacción será interrumpida.
Ejemplo:
@Transactional(timeout = 5) // Máximo 5 segundos
public void executeLongQuery() {
// Operación larga
}
4. ReadOnly (solo lectura)
Si la transacción solo lee datos (sin modificaciones), puedes indicar readOnly=true. Esto puede optimizar el rendimiento.
Ejemplo:
@Transactional(readOnly = true)
public List<Account> getAllAccounts() {
return accountRepository.findAll();
}
5. RollbackFor (rollback de la transacción en caso de error)
Por defecto, Spring revierte la transacción solo en caso de RuntimeException o Error. Si quieres revertir la transacción para otras excepciones (por ejemplo, Exception), debes indicarlo explícitamente:
@Transactional(rollbackFor = Exception.class)
public void processTransaction() throws Exception {
// Código que puede lanzar una excepción
}
Ejemplos reales de uso
Ejemplo 1: Procesamiento de pedidos en una tienda online
Imagina el procesamiento de un pedido, que incluye:
- Restar el producto del stock
- Debitar fondos de la tarjeta del cliente
- Guardar los datos del pedido
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// Comprobación de disponibilidad del producto
stockService.decreaseStock(order.getProductId(), order.getQuantity());
// Debitar fondos
paymentService.charge(order.getPaymentInfo());
// Guardar el pedido
orderRepository.save(order);
}
}
Si en cualquier etapa ocurre un error, todos los cambios se revertirán.
Ejemplo 2: Registro de errores
A veces se requiere guardar un log de eventos fuera de la transacción principal. Usamos REQUIRES_NEW:
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logError(String message) {
errorLogRepository.save(new ErrorLog(message));
}
}
Errores típicos al trabajar con @Transactional
- La anotación no se aplicó. Esto puede pasar si el método es llamado desde la misma clase (a través de
this). Recuerda que Spring aplica proxies, y la llamada interna dentro de la misma clase evita el mecanismo de AOP. ¿Solución? Mueve el método a otro@Service. - Mezclar transacciones externas e internas. Por ejemplo, usar
@Transactional(propagation = Propagation.REQUIRES_NEW)dentro de un método puede generar sobrecarga. Úsalo solo cuando sea necesario. - Comportamiento incorrecto del rollback. Si olvidaste especificar
rollbackFor, Spring puede no revertir la transacción. Siempre define explícitamente el comportamiento que necesitas.
Práctica: Integrar @Transactional en la aplicación
Creamos un método para procesar un pedido con rollback automático en caso de error:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void placeOrder(Long productId, int quantity) {
// Reducimos la cantidad de producto
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("¡Producto no encontrado!"));
if (product.getStock() < quantity) {
throw new RuntimeException("¡No hay suficiente stock!");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// Guardamos el pedido
Order order = new Order();
order.setProductId(productId);
order.setQuantity(quantity);
orderRepository.save(order);
// Falla artificial para comprobar el rollback
if (quantity > 10) {
throw new RuntimeException("¡Fallo al procesar el pedido!");
}
}
}
Comprobación:
- Llamar al método
placeOrdercon datos correctos — creará el pedido y disminuirá el stock. - Ejecutarlo con una cantidad grande (
quantity > 10) — provocará una falla. Spring revertirá los cambios, manteniendo los datos consistentes.
@Transactional — una herramienta potente que nos quita el dolor de gestionar transacciones manualmente. Úsala con responsabilidad, optimiza los parámetros y siempre verifica su funcionamiento mediante tests.
GO TO FULL VERSION