🎯 OBJECTIF
Comprendre comment :
@Transactional@Transactional sur les bonnes frontières métier🧠 MODÈLE MENTAL
Imagine ton bean enveloppé dans une couche invisible — le proxy Spring. Chaque méthode @Transactional est une promesse interceptée : quand l'appel entre par la porte principale (depuis un autre bean), le proxy se réveille, démarre ou rejoint une transaction, puis te passe le relai. Mais si tu appelles directement this.autreMethode() depuis l'intérieur du bean, tu court-circuites le proxy — tu appelles l'objet réel, sans interception. L'annotation est silencieusement ignorée.
La propagation définit la politique de fusion : la méthode appelée rejoint-elle la transaction en cours, en crée-t-elle une indépendante qui survit à un rollback, ou exige-t-elle qu'il n'y en ait aucune ?
Spring implémente @Transactional via un proxy généré au démarrage. Seuls les appels passant par ce proxy déclenchent la gestion transactionnelle. Voir spring-proxies-et-appels-internes pour le mécanisme complet.
sequenceDiagram
participant C as Appelant externe
participant P as Proxy Spring
participant B as Bean (objet réel)
C->>P: createOrder()
P->>P: BEGIN TRANSACTION T1
P->>B: createOrder()
B->>B: this.notifyWarehouse()
Note over B: Appel interne — proxy contourné
Note over B: @Transactional ignoré silencieusement
B-->>P: retour
P->>P: COMMIT T1
P-->>C: réponsemermaid🚨 Appel interne = proxy ignoré
this.method() dans un bean Spring appelle l'objet réel directement, sans passer par le proxy. Toute annotation @Transactional, @Cacheable ou AOP sur cette méthode est silencieusement ignorée — aucune erreur, aucun warning dans les logs.
Fix : extraire la méthode dans un second bean injecté.
// ❌ L'annotation est ignorée
@Service
public class OrderService {
public void createOrder() {
this.notifyWarehouse(); // bypass du proxy
}
@Transactional
public void notifyWarehouse() { ... }
}
// ✓ Passe par le proxy de WarehouseService
@Service
public class OrderService {
private final WarehouseService warehouseService; // bean injecté
public void createOrder() {
warehouseService.notifyWarehouse(); // ✓ proxy traversé
}
}javaLa propagation définit ce qui se passe quand une méthode @Transactional est appelée depuis une transaction existante.
flowchart LR
A["Bean A\nT1 active"] -->|REQUIRED| B1["Bean B\nrejoint T1"]
A -->|REQUIRES_NEW| B2["Bean B\nsuspend T1\ncrée T2 indépendante"]
A -->|NOT_SUPPORTED| B3["Bean B\nsuspend T1\nsans transaction"]
A -->|MANDATORY| B4["Bean B\nexige T1 ✓"]
A2["Bean A\nsans TX"] -->|MANDATORY| B5["Exception ❌"]mermaid| Mode | Comportement | Use-case typique |
|---|---|---|
REQUIRED (défaut) |
Rejoint la TX existante ou en crée une | Cas métier classique |
REQUIRES_NEW |
Suspend la TX courante, crée une TX indépendante | Audit, log, outbox |
SUPPORTS |
Rejoint si TX existe, sinon sans TX | Méthode de lecture réutilisable |
MANDATORY |
Exige une TX existante, exception sinon | Méthode interne critique |
NOT_SUPPORTED |
Suspend toute TX, s'exécute sans TX | Appel API externe |
NEVER |
Exception si une TX existe | Protection contre usage incorrect |
NESTED |
Savepoint dans la TX existante | Rollback partiel dans un batch |
🔑 Cas d'usage REQUIRES_NEW — l'audit qui survit au rollback
L'audit doit être persisté même si la transaction principale échoue. Avec REQUIRES_NEW, le log d'audit commite dans sa propre transaction, indépendamment du sort de T1.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action) {
auditRepo.save(new AuditLog(action)); // ✓ commit même si T1 rollback
}javaC'est aussi le même mécanisme qu'exploite le pattern-outbox-publication-fiable-de-messages-depuis-une-transaction-db pour persister les events dans la même transaction métier tout en garantissant leur publication indépendante.
L'isolation définit ce qu'une transaction peut lire sur les modifications non encore commitées des autres.
| Niveau | Dirty Read | Non-Repeatable Read | Phantom Read | Use-case |
|---|---|---|---|---|
READ_UNCOMMITTED |
✅ | ✅ | ✅ | Quasi jamais |
READ_COMMITTED (défaut PG/Oracle) |
❌ | ✅ | ✅ | La majorité des apps |
REPEATABLE_READ (défaut MySQL) |
❌ | ❌ | ✅ | Calculs sur plusieurs lectures |
SERIALIZABLE |
❌ | ❌ | ❌ | Règles financières critiques |
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<Order> getOrders() { ... }javaIsolation par défaut = celle de la DB
Si tu n'indiques pas d'isolation, Spring délègue à la base : PostgreSQL et Oracle démarrent en READ_COMMITTED, MySQL en REPEATABLE_READ. Ne changer qu'avec un besoin métier précis — SERIALIZABLE a un coût élevé sur les performances.
@Transactional doit être sur la frontière métier — la méthode qui orchestre une unité de travail cohérente.
flowchart TD
UC["UseCase\n@Transactional"] --> RA["OrderRepository"]
UC --> RB["StockRepository"]
UC --> Audit["AuditService\n@Transactional\nREQUIRES_NEW"]mermaid@Transactional sur le use-case@Transactional(REQUIRES_NEW)@Transactional sur le repository si aucune orchestration au-dessus⚡ TL;DR — chaque concept en une ligne
Proxy Spring
✓ Intercepte tous les appels entrants pour gérer démarrage, commit et rollback automatiquement.
⚠ Un appel this.method() court-circuite le proxy — @Transactional est silencieusement ignoré.
REQUIRED (défaut) ✓ Rejoint la transaction existante, ou en crée une si aucune n'est active. ⚠ Tout rollback dans la transaction partagée annule toutes les modifications.
REQUIRES_NEW ✓ Suspend la transaction courante et démarre une transaction indépendante. ⚠ Risque de deadlock si les deux transactions tentent de verrouiller les mêmes lignes.
Isolation ✓ Définit ce qu'une transaction voit des modifications non-commitées des autres. ⚠ Par défaut Spring délègue à la DB — modifier sans raison métier précise crée des surprises.
🎓 À retenir
@Transactional sur une méthode private est silencieusement ignoré — Spring ne peut pas proxifier les méthodes privées quel que soit le type de proxy (JDK ou CGLIB). Aucune erreur, aucune transaction ouverte.REQUIRES_NEW peut causer des deadlocks — si T1 a verrouillé une ligne et T2 (REQUIRES_NEW) essaie de verrouiller la même ligne, T2 attend T1, mais T1 attend que T2 finisse. Deadlock silencieux.@Transactional sur une classe = appliqué à toutes les méthodes publiques — pratique, mais peut ouvrir des transactions sur des méthodes de lecture légères qui n'en ont pas besoin. Préférer l'annotation au niveau méthode pour les use-cases complexes.@Transactional