Contactez-nous

Propagation des transactions (`Propagation`)

Explorez les différents niveaux de propagation des transactions (@Transactional) dans Spring pour contrôler comment les méthodes transactionnelles interagissent entre elles.

Quand une transaction en appelle une autre : Définir les règles du jeu

Dans une application complexe, il est fréquent qu'une méthode de service annotée avec @Transactional appelle une autre méthode, elle aussi potentiellement annotée avec @Transactional (dans le même service ou un service différent). La question cruciale qui se pose alors est : comment ces transactions interagissent-elles ? La seconde méthode doit-elle rejoindre la transaction existante ? Doit-elle démarrer sa propre transaction indépendante ? Doit-elle s'exécuter en dehors de toute transaction ?

C'est précisément ce que définit le comportement de propagation des transactions. Spring offre un contrôle fin sur ce comportement via l'attribut propagation de l'annotation @Transactional. Cet attribut prend une valeur de l'énumération org.springframework.transaction.annotation.Propagation.

Comprendre et choisir le bon niveau de propagation est essentiel pour garantir que les limites de vos transactions (et donc l'atomicité et la cohérence) sont celles que vous attendez, en particulier dans des scénarios d'appels imbriqués.

L'attribut `propagation` de `@Transactional`

La syntaxe pour spécifier le comportement de propagation est la suivante :

@Service
public class MyService {

    @Transactional(propagation = Propagation.PROPAGATION_LEVEL)
    public void myTransactionalMethod() {
        // ... logique métier ...
    }
}

PROPAGATION_LEVEL est l'une des constantes définies dans l'énumération Propagation. Examinons les niveaux les plus importants et leur signification.

Les niveaux de propagation courants

  • Propagation.REQUIRED (Comportement par défaut) : C'est le niveau de propagation le plus couramment utilisé et celui appliqué par défaut si vous n'en spécifiez aucun. Sa logique est :
    • Si une transaction est déjà active lors de l'appel de la méthode, cette méthode rejoint la transaction existante. Elle s'exécute dans le même contexte transactionnel.
    • Si aucune transaction n'est active, une nouvelle transaction est démarrée spécifiquement pour cette méthode.

    Cela signifie que si une méthode A (REQUIRED) appelle une méthode B (REQUIRED), elles partageront la même transaction physique. Un rollback dans B provoquera un rollback de A (et vice-versa). C'est souvent le comportement souhaité pour regrouper des opérations métier liées.

    @Service
    public class ServiceA {
        @Autowired private ServiceB serviceB;
    
        @Transactional // Propagation.REQUIRED par défaut
        public void operationA() {
            // ... logique A ...
            serviceB.operationB(); // S'exécute dans la même transaction que operationA
            // ... autre logique A ...
        }
    }
    
    @Service
    public class ServiceB {
        @Transactional // Propagation.REQUIRED par défaut
        public void operationB() {
            // ... logique B ...
            // Si une exception est levée ici, toute l'operationA sera rollbackée.
        }
    }
    
  • Propagation.REQUIRES_NEW : Ce niveau indique que la méthode doit toujours démarrer sa propre transaction physique, indépendante.
    • Si une transaction est déjà active lors de l'appel, la transaction existante est suspendue. Une nouvelle transaction est démarrée pour la méthode appelée. Une fois la méthode terminée (commit ou rollback), la nouvelle transaction se termine et la transaction initiale est reprise.
    • Si aucune transaction n'est active, une nouvelle transaction est démarrée (similaire à REQUIRED dans ce cas).

    REQUIRES_NEW est utile lorsque vous voulez qu'une opération s'exécute dans sa propre unité atomique, indépendamment du succès ou de l'échec de la transaction appelante. Un cas d'usage typique est l'enregistrement d'un log d'audit qui doit persister même si la transaction principale échoue.

    @Service
    public class MainService {
        @Autowired private AuditService auditService;
    
        @Transactional // REQUIRED
        public void processData() {
            try {
               // ... logique principale ...
               if (someCondition) throw new RuntimeException("Erreur métier");
               // ...
               auditService.logSuccess("Données traitées"); // Appel avec REQUIRES_NEW
            } catch (Exception e) {
               auditService.logFailure("Echec traitement: " + e.getMessage()); // Appel avec REQUIRES_NEW
               throw e; // Relancer pour que la transaction principale soit rollbackée
            }
        }
    }
    
    @Service
    public class AuditService {
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void logSuccess(String message) {
            // Ecrit le log dans la BDD (transaction indépendante)
            // Ce log persistera même si processData échoue plus tard.
        }
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void logFailure(String message) {
            // Ecrit le log dans la BDD (transaction indépendante)
            // Ce log persistera même si la transaction de processData est rollbackée.
        }
    }
    

    Attention : Utiliser REQUIRES_NEW peut avoir des implications sur les performances (création de nouvelles connexions/transactions) et sur le verrouillage (la transaction suspendue peut conserver des verrous).

Autres niveaux de propagation

  • Propagation.SUPPORTS :
    • Si une transaction est active, la méthode la rejoint (comme REQUIRED).
    • Si aucune transaction n'est active, la méthode s'exécute sans transaction.

    Utile pour des méthodes (souvent en lecture seule) qui peuvent bénéficier d'une transaction si elle existe (par exemple, pour la cohérence de lecture) mais qui n'en nécessitent pas absolument une.

  • Propagation.MANDATORY :
    • Si une transaction est active, la méthode la rejoint.
    • Si aucune transaction n'est active, une exception est levée.

    Utile pour s'assurer qu'une méthode ne soit jamais appelée en dehors d'un contexte transactionnel défini.

  • Propagation.NOT_SUPPORTED :
    • Si une transaction est active, elle est suspendue, et la méthode s'exécute sans transaction.
    • Si aucune transaction n'est active, la méthode s'exécute simplement sans transaction.

    Utile pour appeler des méthodes qui ne doivent pas participer à la transaction en cours (par exemple, des opérations longues, des accès à des ressources non transactionnelles, ou des actions qui doivent se produire indépendamment du commit/rollback).

  • Propagation.NEVER :
    • Si une transaction est active, une exception est levée.
    • Si aucune transaction n'est active, la méthode s'exécute sans transaction.

    Utile pour garantir qu'une méthode ne soit jamais impliquée dans une transaction appelante.

  • Propagation.NESTED :
    • Si une transaction est active, la méthode démarre une "transaction imbriquée". Cela fonctionne différemment de REQUIRES_NEW. Elle utilise le concept de savepoints JDBC (si supporté par le driver et la base de données). Un rollback de la transaction imbriquée restaure l'état au savepoint créé au début de la méthode, sans nécessairement invalider la transaction externe. Un commit de la transaction imbriquée ne fait rien immédiatement, le vrai commit n'aura lieu qu'à la fin de la transaction externe.
    • Si aucune transaction n'est active, elle se comporte comme REQUIRED (démarre une nouvelle transaction).

    NESTED est moins fréquemment utilisé et dépend fortement du support sous-jacent (principalement via DataSourceTransactionManager avec JDBC). Il n'est généralement pas supporté par les gestionnaires de transactions JTA.

Choisir le bon niveau de propagation

Le choix du niveau de propagation dépend entièrement de la sémantique souhaitée pour votre opération métier :

  • REQUIRED (Défaut) : Utilisez-le pour la plupart des opérations métier qui doivent faire partie d'une même unité atomique.
  • REQUIRES_NEW : Réservez-le aux opérations qui doivent impérativement avoir leur propre résultat transactionnel indépendant, comme l'audit, la journalisation robuste, ou l'envoi de notifications critiques, même en cas d'échec de l'opération principale. Soyez conscient des impacts potentiels sur les performances et le verrouillage.
  • SUPPORTS : Convient aux méthodes de lecture qui peuvent s'exécuter avec ou sans transaction.
  • NOT_SUPPORTED : Utile pour exécuter du code non transactionnel depuis un contexte transactionnel.
  • MANDATORY / NEVER : Pour des assertions fortes sur le contexte transactionnel d'appel.
  • NESTED : Cas d'usage plus spécifiques nécessitant des rollbacks partiels via savepoints, avec une forte dépendance au support JDBC.

Bien comprendre la propagation est essentiel pour maîtriser le comportement transactionnel de votre application Spring, en particulier lorsque les services s'appellent mutuellement.