En la última clase vimos los tipos de excepciones en transacciones y los mecanismos básicos para tratarlas en Spring. Ahora toca profundizar en la práctica y ver escenarios más complejos de uso de rollback.
¿Por qué es importante configurar bien el rollback?
Configurar mal los rollbacks puede causar problemas serios:
- Cambios incompletos: parte de los datos puede quedar en un estado inconsistente, por ejemplo, al transferir dinero entre cuentas.
- Violación de reglas de negocio: el sistema puede guardar cambios que contradigan la lógica de negocio (por ejemplo, un pedido que supera el límite).
- Problemas de escalabilidad: en sistemas multiusuario, un manejo incorrecto de transacciones provoca errores difíciles de rastrear.
Spring ofrece herramientas flexibles para manejar el rollback de transacciones. Vamos a aprender a usarlas.
Escenarios prácticos de uso de rollback
En aplicaciones reales a menudo hay situaciones en las que el comportamiento por defecto de las transacciones no basta. Veamos, con el ejemplo de una tienda online, cómo gestionar transacciones de forma flexible en distintos casos.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductService productService;
@Transactional
public void processOrder(Order order) {
// Comprobamos disponibilidad del producto
if (!productService.isAvailable(order.getProductId())) {
// Rollback programático: indicamos explícitamente a Spring que haga rollback de la transacción
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new ProductNotAvailableException("Producto no disponible");
}
// Guardamos el pedido
orderRepository.save(order);
// Comprobamos los límites del usuario
if (orderRepository.getUserOrderCount(order.getUserId()) > 5) {
// Rollback automático: Spring hará rollback de la transacción al lanzar la excepción
throw new BusinessException("Se ha superado el límite de pedidos");
}
// Reducimos el stock y manejamos casos especiales
try {
productService.decreaseStock(order.getProductId(), order.getQuantity());
} catch (StockWarningException e) {
// Registramos la advertencia, pero dejamos que la transacción termine
notifyManager(e);
}
}
// Ejemplo donde algunos errores no deben cancelar toda la operación
@Transactional(noRollbackFor = {StockWarningException.class})
public void processOrderWithPartialErrors(Order order) {
orderRepository.save(order);
productService.decreaseStock(order.getProductId(), order.getQuantity());
// Incluso si ocurre StockWarningException,
// el pedido permanecerá guardado
}
}
En este ejemplo vemos tres enfoques distintos para manejar transacciones:
- Rollback programático vía
setRollbackOnly()— cuando necesitamos control total del proceso - Rollback automático al lanzar una excepción — comportamiento por defecto de Spring para RuntimeException
- Rollback selectivo con
noRollbackFor— cuando algunos errores no deberían cancelar toda la operación
Cada enfoque tiene sus ventajas:
- El rollback programático da máximo control
- El rollback automático hace el código más limpio y fácil de entender
- El rollback selectivo permite manejar de forma flexible distintos tipos de errores
Pruebas de rollback
Para comprobar que las transacciones funcionan correctamente, Spring ofrece herramientas de testing. Veamos cómo podemos probar la lógica de procesar pedidos:
@SpringBootTest
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
@Transactional
public void testOrderRollbackOnLimitExceeded() {
// Preparación
Order order = new Order("Product A", 1);
// Acción
assertThrows(BusinessException.class, () -> {
orderService.processOrder(order);
});
// Verificación
assertEquals(0, orderRepository.count(),
"La base de datos debe estar vacía después del rollback de la transacción");
}
@Test
@Transactional
public void testPartialRollback() {
Order order = new Order("Product B", 1);
// Este método no hace rollback en caso de StockWarningException
orderService.processOrderWithPartialErrors(order);
// Comprobamos que el pedido se ha guardado a pesar de las posibles advertencias
assertTrue(orderRepository.existsById(order.getId()));
}
}
Fíjate:
- La anotación
@Transactionalen tests hace rollback automáticamente de todos los cambios después de cada test - Esto nos evita preocuparnos por limpiar la base de datos entre tests
- Podemos comprobar fácilmente cómo funciona el rollback en distintos escenarios
Errores típicos al trabajar con rollback
Trabajar con rollback puede ser tanto una bendición como una fuente de problemas si no se tiene cuidado:
1. Capturar excepciones manualmente: si capturas la excepción dentro de un método transaccional, Spring no sabrá del error y la transacción terminará con éxito.
Ejemplo:
@Transactional
public void incorrectTransactionHandling() {
try {
saveDataToDatabase();
throw new RuntimeException("Error");
} catch (Exception e) {
// ¡La excepción fue capturada, la transacción no hará rollback!
}
}
2. Llamar a un método transaccional usando this: Spring usa proxies para manejar las transacciones. Si llamas a un método transaccional desde otro método de la misma clase usando this, la anotación @Transactional se ignora.
public class OrderService {
@Transactional
public void methodA() {
// La transacción funcionará
}
public void methodB() {
this.methodA(); // La transacción no funcionará
}
}
Solución: inyecta el servicio en sí mismo o mueve la llamada a otra clase.
3. Operaciones fuera de la transacción: si modificas datos fuera de la transacción, no se harán rollback. Por ejemplo, llamadas a APIs externas o cambios en variables estáticas quedan fuera de la transacción.
Conclusión
El rollback de transacciones es un mecanismo potente en Spring que permite garantizar la consistencia de los datos en caso de fallos o errores de negocio. Pero, como con cualquier herramienta, hay que usar los rollbacks con cabeza. Espero que ya no le tengas miedo a la palabra rollback — no es un error, es un salvavidas cuando algo va mal.
GO TO FULL VERSION