Saltar al contenido principal

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á:

  1. ✅ Comprender las propiedades ACID
  2. ✅ Utilizar @Transactional correctamente
  3. ✅ Elegir el nivel de propagación adecuado
  4. ✅ Comprender los niveles de aislamiento
  5. ✅ Gestionar los rollbacks correctamente
  6. ✅ 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ónDescripción
REQUIREDReutiliza o crea (por defecto)
REQUIRES_NEWSiempre nueva transacción
SUPPORTSTransacción si ya existe, si no, no transaccional
MANDATORYTransacción obligatoria (excepción si no)
NOT_SUPPORTEDSuspende la transacción existente
NEVERExcepción si existe transacción
NESTEDTransacció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​

NivelDirty ReadNon-RepeatablePhantom
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) { }
precaución

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


⬅️ Volver al Módulo 7