Capítulo 2: Gestión de Transacciones (@Transactional)
Introducción
Imagine un giro bancario: usted debita 100 € de la cuenta A y acredita 100 € en la cuenta B. ¿Qué sucede si el abono falla después del débito? ¡Sin transacción, los 100 € desaparecen!
Las transacciones garantizan que sus operaciones de datos sean fiables y coherentes.
🎯 Objetivos del Capítulo
Al final de este capítulo, usted sabrá:
- ✅ Comprender las propiedades ACID
- ✅ Utilizar @Transactional correctamente
- ✅ Elegir el nivel de propagación adecuado
- ✅ Comprender los niveles de aislamiento
- ✅ Gestionar los rollbacks correctamente
- ✅ Evitar trampas comunes
1. ¿Qué es una Transacción?
Definición
Una transacción es un conjunto de operaciones que deben todas tener éxito o todas fallar.
Ejemplo Concreto: Giro Bancario
// SIN transacción (¡PELIGROSO!)
public void transfert(Long compteA, Long compteB, BigDecimal montant) {
compteRepo.debiter(compteA, montant); // ← Exitoso: -100 €
// 💥 ERROR AQUÍ (red, excepción, etc.)
compteRepo.crediter(compteB, montant); // ← ¡Nunca ejecutado!
// Resultado: ¡100 € han DESAPARECIDO! 💀
}
// CON transacción (SEGURO)
@Transactional
public void transfert(Long compteA, Long compteB, BigDecimal montant) {
compteRepo.debiter(compteA, montant); // ← Dentro de la transacción
// 💥 ERROR AQUÍ
compteRepo.crediter(compteB, montant); // ← Nunca ejecutado
// Resultado: ROLLBACK automático, el débito es ANULADO ✓
}
2. Las Propiedades ACID
Cada transacción respeta 4 propiedades fundamentales:
A - Atomicidad (Atomicity)
"Todo o nada"
✅ Todas las operaciones tienen éxito → COMMIT
❌ Una sola operación falla → ROLLBACK de TODO
C - Coherencia (Consistency)
"La base de datos permanece en un estado válido"
Antes: Cuenta A = 500 €, Cuenta B = 300 €, Total = 800 €
Después: Cuenta A = 400 €, Cuenta B = 400 €, Total = 800 €
El dinero no desaparece ni aparece mágicamente.
I - Aislamiento (Isolation)
"Las transacciones no se ven entre sí"
Transacción 1: Lee el saldo = 500 €
Transacción 2: Modifica el saldo = 400 €
Transacción 1: Lee de nuevo = 500 € (o 400 € según el aislamiento)
D - Durabilidad (Durability)
"Una vez COMMIT, es definitivo"
Transacción COMMIT ✓
→ Incluso si el servidor falla después, los datos están guardados.
3. @Transactional en Detalle
Uso Básico
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// Todas estas operaciones están en UNA transacción
orderRepo.save(order);
inventoryService.decreaseStock(order.getItems());
paymentService.processPayment(order.getTotal());
emailService.sendConfirmation(order);
}
}
¿Cómo Funciona?
Spring utiliza un proxy alrededor de su clase:
¿Dónde Colocar @Transactional?
// ✅ RECOMENDADO: En el método de servicio
@Service
public class UserService {
@Transactional
public void createUser(User user) {
// ...
}
}
// ✅ OK: En la clase (todos los métodos públicos)
@Service
@Transactional
public class UserService {
public void createUser(User user) { /* transaccional */ }
public void updateUser(User user) { /* transaccional */ }
}
// ❌ NO FUNCIONA: En métodos privados
@Service
public class UserService {
@Transactional // ← ¡IGNORADO!
private void internalOperation() {
// El proxy no puede interceptar métodos privados
}
}
4. Niveles de Propagación
La propagación define cómo se comporta una transacción cuando llama a otro método transaccional.
REQUIRED (Por Defecto)
"Reutiliza la transacción existente, o crea una nueva"
@Transactional(propagation = Propagation.REQUIRED) // Por defecto
public void methodA() {
methodB(); // ← Reutiliza la transacción de methodA
}
@Transactional
public void methodB() {
// Misma transacción que methodA
}
methodA() BEGIN TRANSACTION
├── operaciones de methodA
├── methodB() [misma transacción]
│ └── operaciones de methodB
└── COMMIT (o ROLLBACK global)
REQUIRES_NEW
"Siempre crea una nueva transacción"
@Transactional
public void methodA() {
// Transacción 1
repo.save(entity);
loggingService.logAction("action"); // ← Transacción 2 (independiente)
throw new RuntimeException(); // ← Rollback Transacción 1
// Pero el log se CONSERVA (Transacción 2 commitida)
}
@Service
public class LoggingService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action) {
// Nueva transacción independiente
logRepo.save(new Log(action));
}
}
methodA() BEGIN TRANSACTION 1
├── save entity
├── logAction() BEGIN TRANSACTION 2
│ ├── save log
│ └── COMMIT TRANSACTION 2 ✓
├── throw exception
└── ROLLBACK TRANSACTION 1 (entity anulado, log conservado)
Otras Propagaciones
| Propagación | Descripción |
|---|---|
| REQUIRED | Reutiliza o crea (por defecto) |
| REQUIRES_NEW | Siempre nueva transacción |
| SUPPORTS | Transacción si ya existe, si no, no transaccional |
| MANDATORY | Transacción obligatoria (excepción si no) |
| NOT_SUPPORTED | Suspende la transacción existente |
| NEVER | Excepción si existe transacción |
| NESTED | Transacción anidada (savepoints) |
5. Niveles de Aislamiento
El aislamiento define cómo las transacciones ven las modificaciones de otras transacciones.
Problemas de Aislamiento
1. Dirty Read (Lectura Sucia)
Transacción A: UPDATE saldo = 400 (aún no COMMIT)
Transacción B: SELECT saldo → 400 (¡lee un valor no commitido!)
Transacción A: ROLLBACK
Resultado: B leyó un valor que ya no existe
2. Non-Repeatable Read (Lectura No Repetible)
Transacción A: SELECT saldo → 500
Transacción B: UPDATE saldo = 400, COMMIT
Transacción A: SELECT saldo → 400 (¡valor diferente!)
3. Phantom Read (Lectura Fantasma)
Transacción A: SELECT COUNT(*) WHERE status='ACTIVE' → 10
Transacción B: INSERT INTO users (status='ACTIVE'), COMMIT
Transacción A: SELECT COUNT(*) WHERE status='ACTIVE' → 11 (¡nuevo registro!)
Niveles de Aislamiento
| Nivel | Dirty Read | Non-Repeatable | Phantom |
|---|---|---|---|
| READ_UNCOMMITTED | ❌ Posible | ❌ Posible | ❌ Posible |
| READ_COMMITTED | ✅ Impedido | ❌ Posible | ❌ Posible |
| REPEATABLE_READ | ✅ Impedido | ✅ Impedido | ❌ Posible |
| SERIALIZABLE | ✅ Impedido | ✅ Impedido | ✅ Impedido |
Uso
// Por defecto: READ_COMMITTED (según la BDD)
@Transactional
public void normalOperation() { }
// Para reportes financieros: SERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public BigDecimal calculateTotalBalance() { }
// Para lecturas simples: READ_COMMITTED es suficiente
@Transactional(isolation = Isolation.READ_COMMITTED)
public User getUser(Long id) { }
SERIALIZABLE es el más seguro pero el más lento. ¡Úselo solo cuando sea realmente necesario!
6. Rollback y Excepciones
Comportamiento por Defecto
@Transactional
public void operation() {
repo.save(entity);
throw new RuntimeException(); // ✅ ROLLBACK automático
}
@Transactional
public void operation() throws IOException {
repo.save(entity);
throw new IOException(); // ❌ ¡NO hay rollback! (checked exception)
}
Regla por defecto:
- ✅ RuntimeException (unchecked) → Rollback
- ❌ Exception (checked) → No hay rollback
Forzar el Rollback
// Rollback en todas las excepciones
@Transactional(rollbackFor = Exception.class)
public void operation() throws Exception {
repo.save(entity);
throw new IOException(); // ✅ ROLLBACK ahora
}
// Rollback en excepciones específicas
@Transactional(rollbackFor = {IOException.class, CustomException.class})
public void operation() throws Exception { }
No Hacer Rollback
// No hacer rollback en ciertas excepciones
@Transactional(noRollbackFor = BusinessException.class)
public void operation() {
repo.save(entity);
throw new BusinessException("Regla de negocio violada");
// No hay rollback, la entidad se guarda
}
7. Transacciones de Solo Lectura
Optimización para Lecturas
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepo.findAll();
}
@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepo.findById(id).orElseThrow();
}
Ventajas:
- Hibernate desactiva el dirty checking (más rápido)
- Algunas BDD optimizan las consultas read-only
- Documentación explícita de la intención
A Nivel de Clase
@Service
@Transactional(readOnly = true) // Por defecto: solo lectura
public class UserService {
public List<User> getAllUsers() { } // readOnly = true
public User getUser(Long id) { } // readOnly = true
@Transactional // Override: lectura-escritura
public void createUser(User user) { }
}
8. Timeout
Limitar la Duración de una Transacción
@Transactional(timeout = 5) // Máximo 5 segundos
public void longOperation() {
// Si tarda más de 5 segundos → TransactionTimedOutException
}
Casos de uso:
- Consultas complejas que pueden bloquearse
- Protección contra deadlocks
- Garantizar la capacidad de respuesta del sistema
9. Trampas Comunes
❌ Trampa 1: Self-Invocation (¡La Más Frecuente!)
@Service
public class UserService {
public void methodA() {
this.methodB(); // ❌ @Transactional IGNORADO!
}
@Transactional
public void methodB() {
// No hay transacción porque se llama desde la misma clase
}
}
¿Por qué? El proxy solo puede interceptar llamadas externas.
Solución:
@Service
public class UserService {
@Autowired
private UserService self; // Inyección de sí mismo
public void methodA() {
self.methodB(); // ✅ Pasa por el proxy
}
@Transactional
public void methodB() {
// ¡Transacción activa!
}
}
❌ Trampa 2: Excepción Capturada
@Transactional
public void operation() {
try {
riskyOperation();
} catch (Exception e) {
log.error("Error ignorado", e); // ❌ Excepción capturada
// ¡La transacción no sabe que hubo un error!
}
}
Solución:
@Transactional
public void operation() {
try {
riskyOperation();
} catch (Exception e) {
log.error("Error", e);
throw e; // ✅ Relanzar para disparar el rollback
}
}
❌ Trampa 3: @Transactional en Interfaz
// ❌ NO FUNCIONA con proxies CGLIB (por defecto)
public interface UserService {
@Transactional
void createUser(User user);
}
// ✅ Ponga la anotación en la implementación
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void createUser(User user) { }
}
❌ Trampa 4: Método Final
@Service
public class UserService {
@Transactional
public final void operation() { // ❌ CGLIB no puede crear el proxy
// ¡Transacción ignorada!
}
}
Solución: Retire la palabra clave final.
10. Resumen
Resumen de los Atributos
@Transactional(
propagation = Propagation.REQUIRED, // Comportamiento de transacción
isolation = Isolation.READ_COMMITTED, // Nivel de aislamiento
timeout = 30, // Timeout en segundos
readOnly = false, // ¿Solo lectura?
rollbackFor = Exception.class, // ¿Rollback en qué excepciones?
noRollbackFor = BusinessException.class // ¿No hacer rollback en qué excepciones?
)
public void operation() { }
Configuraciones Comunes
// Creación/Modificación estándar
@Transactional
public void createUser(User user) { }
// Lectura optimizada
@Transactional(readOnly = true)
public User getUser(Long id) { }
// Operación crítica (auditoría)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String action) { }
// Procesamiento de pago
@Transactional(
isolation = Isolation.SERIALIZABLE,
rollbackFor = Exception.class
)
public void processPayment(Payment payment) { }
Checklist
✅ Poner @Transactional en los métodos de servicio (no en controller)
✅ Usar readOnly = true para lecturas
✅ Comprender la propagación utilizada
✅ Verificar que las excepciones disparan el rollback
✅ Evitar la self-invocation
✅ No capturar las excepciones
🎯 Próximo Paso
Ahora que domina las transacciones, aprendamos a versionar su esquema de base de datos con Flyway.
➡️ Capítulo 3: Migración de Esquema con Flyway