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:
- ✅ Comprendre les propriétés ACID
- ✅ Utiliser @Transactional correctement
- ✅ Choisir le bon niveau de propagation
- ✅ Comprendre les niveaux d'isolation
- ✅ Gérer les rollbacks correctement
- ✅ É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
| Propagation | Description |
|---|---|
| REQUIRED | Réutilise ou crée (par défaut) |
| REQUIRES_NEW | Toujours nouvelle transaction |
| SUPPORTS | Transaction si déjà existante, sinon non-transactionnel |
| MANDATORY | Transaction obligatoire (exception sinon) |
| NOT_SUPPORTED | Suspend la transaction existante |
| NEVER | Exception si transaction existante |
| NESTED | Transaction 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
| Niveau | Dirty Read | Non-Repeatable | Phantom |
|---|---|---|---|
| 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) { }
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