Aller au contenu principal

Chapitre 2: Gestion des Transactions (@Transactional)

Introduction​

Imaginez un virement bancaire: vous débitez 100€ du compte A et créditez 100€ sur le compte B. Que se passe-t-il si le crédit échoue après le débit? Sans transaction, les 100€ disparaissent!

Les transactions garantissent que vos opérations de données sont fiables et cohérentes.


🎯 Objectifs du Chapitre​

À la fin de ce chapitre, vous saurez:

  1. ✅ Comprendre les propriétés ACID
  2. ✅ Utiliser @Transactional correctement
  3. ✅ Choisir le bon niveau de propagation
  4. ✅ Comprendre les niveaux d'isolation
  5. ✅ Gérer les rollbacks correctement
  6. ✅ Éviter les pièges courants

1. Qu'est-ce qu'une Transaction?​

Définition​

Une transaction est un ensemble d'opérations qui doivent toutes réussir ou toutes échouer.

Exemple Concret: Virement Bancaire​

// SANS transaction (DANGEREUX!)
public void transfert(Long compteA, Long compteB, BigDecimal montant) {
compteRepo.debiter(compteA, montant); // ← Réussi: -100€

// 💥 ERREUR ICI (réseau, exception, etc.)

compteRepo.crediter(compteB, montant); // ← Jamais exécuté!

// Résultat: 100€ ont DISPARU! 💀
}
// AVEC transaction (SÉCURISÉ)
@Transactional
public void transfert(Long compteA, Long compteB, BigDecimal montant) {
compteRepo.debiter(compteA, montant); // ← Dans la transaction

// 💥 ERREUR ICI

compteRepo.crediter(compteB, montant); // ← Jamais exécuté

// Résultat: ROLLBACK automatique, le débit est ANNULÉ ✓
}

2. Les Propriétés ACID​

Chaque transaction respecte 4 propriétés fondamentales:

A - Atomicité (Atomicity)​

"Tout ou rien"

✅ Toutes les opérations réussissent → COMMIT
❌ Une seule opération échoue → ROLLBACK de TOUT

C - Cohérence (Consistency)​

"La base de données reste dans un état valide"

Avant: Compte A = 500€, Compte B = 300€, Total = 800€
Après: Compte A = 400€, Compte B = 400€, Total = 800€

L'argent ne disparaît pas et n'apparaît pas magiquement.

I - Isolation​

"Les transactions ne se voient pas entre elles"

Transaction 1: Lit le solde = 500€
Transaction 2: Modifie le solde = 400€
Transaction 1: Lit encore = 500€ (ou 400€ selon l'isolation)

D - Durabilité (Durability)​

"Une fois COMMIT, c'est définitif"

Transaction COMMIT ✓
→ Même si le serveur plante après, les données sont sauvegardées.

3. @Transactional en Détail​

Utilisation de Base​

import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

@Transactional
public void createOrder(Order order) {
// Toutes ces opérations sont dans UNE transaction
orderRepo.save(order);
inventoryService.decreaseStock(order.getItems());
paymentService.processPayment(order.getTotal());
emailService.sendConfirmation(order);
}
}

Comment ça Marche?​

Spring utilise un proxy autour de votre classe:

Où Placer @Transactional?​

// ✅ RECOMMANDÉ: Sur la méthode de service
@Service
public class UserService {

@Transactional
public void createUser(User user) {
// ...
}
}

// ✅ OK: Sur la classe (toutes les méthodes publiques)
@Service
@Transactional
public class UserService {

public void createUser(User user) { /* transactionnel */ }
public void updateUser(User user) { /* transactionnel */ }
}

// ❌ NE FONCTIONNE PAS: Sur les méthodes privées
@Service
public class UserService {

@Transactional // ← IGNORÉ!
private void internalOperation() {
// Le proxy ne peut pas intercepter les méthodes privées
}
}

4. Niveaux de Propagation​

La propagation définit comment une transaction se comporte quand elle appelle une autre méthode transactionnelle.

REQUIRED (Par Défaut)​

"Réutilise la transaction existante, ou en crée une nouvelle"

@Transactional(propagation = Propagation.REQUIRED)  // Par défaut
public void methodA() {
methodB(); // ← Réutilise la transaction de methodA
}

@Transactional
public void methodB() {
// Même transaction que methodA
}
methodA() BEGIN TRANSACTION
├── opérations de methodA
├── methodB() [même transaction]
│ └── opérations de methodB
└── COMMIT (ou ROLLBACK global)

REQUIRES_NEW​

"Toujours créer une nouvelle transaction"

@Transactional
public void methodA() {
// Transaction 1
repo.save(entity);

loggingService.logAction("action"); // ← Transaction 2 (indépendante)

throw new RuntimeException(); // ← Rollback Transaction 1
// Mais le log est CONSERVÉ (Transaction 2 commitée)
}

@Service
public class LoggingService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action) {
// Nouvelle transaction indépendante
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 annulé, log conservé)

Autres Propagations​

PropagationDescription
REQUIREDRéutilise ou crée (par défaut)
REQUIRES_NEWToujours nouvelle transaction
SUPPORTSTransaction si déjà existante, sinon non-transactionnel
MANDATORYTransaction obligatoire (exception sinon)
NOT_SUPPORTEDSuspend la transaction existante
NEVERException si transaction existante
NESTEDTransaction imbriquée (savepoints)

5. Niveaux d'Isolation​

L'isolation définit comment les transactions voient les modifications des autres transactions.

Les Problèmes d'Isolation​

1. Dirty Read (Lecture Sale)​

Transaction A: UPDATE solde = 400 (pas encore COMMIT)
Transaction B: SELECT solde → 400 (lit une valeur non commitée!)
Transaction A: ROLLBACK
Résultat: B a lu une valeur qui n'existe plus

2. Non-Repeatable Read (Lecture Non Répétable)​

Transaction A: SELECT solde → 500
Transaction B: UPDATE solde = 400, COMMIT
Transaction A: SELECT solde → 400 (valeur différente!)

3. Phantom Read (Lecture Fantôme)​

Transaction A: SELECT COUNT(*) WHERE status='ACTIVE' → 10
Transaction B: INSERT INTO users (status='ACTIVE'), COMMIT
Transaction A: SELECT COUNT(*) WHERE status='ACTIVE' → 11 (nouveau enregistrement!)

Les Niveaux d'Isolation​

NiveauDirty ReadNon-RepeatablePhantom
READ_UNCOMMITTED❌ Possible❌ Possible❌ Possible
READ_COMMITTED✅ Empêché❌ Possible❌ Possible
REPEATABLE_READ✅ Empêché✅ Empêché❌ Possible
SERIALIZABLE✅ Empêché✅ Empêché✅ Empêché

Utilisation​

// Par défaut: READ_COMMITTED (selon la BDD)
@Transactional
public void normalOperation() { }

// Pour les rapports financiers: SERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public BigDecimal calculateTotalBalance() { }

// Pour les lectures simples: READ_COMMITTED suffit
@Transactional(isolation = Isolation.READ_COMMITTED)
public User getUser(Long id) { }
attention

SERIALIZABLE est le plus sûr mais le plus lent. Utilisez-le uniquement quand c'est vraiment nécessaire!


6. Rollback et Exceptions​

Comportement par Défaut​

@Transactional
public void operation() {
repo.save(entity);

throw new RuntimeException(); // ✅ ROLLBACK automatique
}

@Transactional
public void operation() throws IOException {
repo.save(entity);

throw new IOException(); // ❌ PAS de rollback! (checked exception)
}

Règle par défaut:

  • ✅ RuntimeException (unchecked) → Rollback
  • ❌ Exception (checked) → Pas de rollback

Forcer le Rollback​

// Rollback sur toutes les exceptions
@Transactional(rollbackFor = Exception.class)
public void operation() throws Exception {
repo.save(entity);
throw new IOException(); // ✅ ROLLBACK maintenant
}

// Rollback sur des exceptions spécifiques
@Transactional(rollbackFor = {IOException.class, CustomException.class})
public void operation() throws Exception { }

Ne PAS Rollback​

// Ne pas rollback sur certaines exceptions
@Transactional(noRollbackFor = BusinessException.class)
public void operation() {
repo.save(entity);
throw new BusinessException("Règle métier violée");
// Pas de rollback, l'entité est sauvegardée
}

7. Transactions en Lecture Seule​

Optimisation pour les Lectures​

@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepo.findAll();
}

@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepo.findById(id).orElseThrow();
}

Avantages:

  • Hibernate désactive le dirty checking (plus rapide)
  • Certaines BDD optimisent les requêtes read-only
  • Documentation explicite de l'intention

Au Niveau de la Classe​

@Service
@Transactional(readOnly = true) // Par défaut: lecture seule
public class UserService {

public List<User> getAllUsers() { } // readOnly = true
public User getUser(Long id) { } // readOnly = true

@Transactional // Override: lecture-écriture
public void createUser(User user) { }
}

8. Timeout​

Limiter la Durée d'une Transaction​

@Transactional(timeout = 5)  // 5 secondes max
public void longOperation() {
// Si ça prend plus de 5 secondes → TransactionTimedOutException
}

Cas d'utilisation:

  • Requêtes complexes qui peuvent bloquer
  • Protection contre les deadlocks
  • Garantir la réactivité du système

9. Pièges Courants​

❌ Piège 1: Self-Invocation (Le Plus Fréquent!)​

@Service
public class UserService {

public void methodA() {
this.methodB(); // ❌ @Transactional IGNORÉ!
}

@Transactional
public void methodB() {
// Pas de transaction car appelé depuis la même classe
}
}

Pourquoi? Le proxy ne peut intercepter que les appels externes.

Solution:

@Service
public class UserService {

@Autowired
private UserService self; // Injection de soi-même

public void methodA() {
self.methodB(); // ✅ Passe par le proxy
}

@Transactional
public void methodB() {
// Transaction active!
}
}

❌ Piège 2: Exception Avalée​

@Transactional
public void operation() {
try {
riskyOperation();
} catch (Exception e) {
log.error("Erreur ignorée", e); // ❌ Exception avalée
// La transaction ne sait pas qu'il y a eu une erreur!
}
}

Solution:

@Transactional
public void operation() {
try {
riskyOperation();
} catch (Exception e) {
log.error("Erreur", e);
throw e; // ✅ Re-throw pour déclencher le rollback
}
}

❌ Piège 3: @Transactional sur Interface​

// ❌ NE FONCTIONNE PAS avec les proxies CGLIB (par défaut)
public interface UserService {
@Transactional
void createUser(User user);
}

// ✅ Mettez l'annotation sur l'implémentation
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void createUser(User user) { }
}

❌ Piège 4: Méthode Finale​

@Service
public class UserService {

@Transactional
public final void operation() { // ❌ CGLIB ne peut pas créer le proxy
// Transaction ignorée!
}
}

Solution: Retirez le mot-clé final.


10. Résumé​

Récapitulatif des Attributs​

@Transactional(
propagation = Propagation.REQUIRED, // Comportement de transaction
isolation = Isolation.READ_COMMITTED, // Niveau d'isolation
timeout = 30, // Timeout en secondes
readOnly = false, // Lecture seule?
rollbackFor = Exception.class, // Rollback sur quelles exceptions?
noRollbackFor = BusinessException.class // Pas de rollback sur quelles exceptions?
)
public void operation() { }

Configurations Courantes​

// Création/Modification standard
@Transactional
public void createUser(User user) { }

// Lecture optimisée
@Transactional(readOnly = true)
public User getUser(Long id) { }

// Opération critique (audit)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String action) { }

// Traitement de paiement
@Transactional(
isolation = Isolation.SERIALIZABLE,
rollbackFor = Exception.class
)
public void processPayment(Payment payment) { }

Checklist​

✅ Mettre @Transactional sur les méthodes de service (pas controller)
✅ Utiliser readOnly = true pour les lectures
✅ Comprendre la propagation utilisée
✅ Vérifier que les exceptions déclenchent le rollback
✅ Éviter la self-invocation
✅ Ne pas avaler les exceptions

🎯 Prochaine Étape​

Maintenant que vous maîtrisez les transactions, apprenons à versionner votre schéma de base de données avec Flyway.

➡️ Chapitre 3: Migration de Schéma avec Flyway


⬅️ Retour au Module 7