• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 15/12/2020

Gérer les transactions avec Spring TX

Je vous disais dans l'introduction que Spring était un framework modulaire. Et ce qui est génial c'est que les modules fonctionnent très bien ensemble.

Dans le chapitre précédent nous avons vu comment exécuter des requêtes SQL avec Spring JDBC.

Eh bien, sachez que Spring JDBC peut fonctionner dans un contexte transactionnel, lui aussi géré avec Spring ! C'est le rôle du module Spring TX.

Gérer le contexte transactionnel

Vous commencez à avoir l'habitude que Spring vous laisse le choix... Avec Spring TX, vous pouvez gérer plusieurs types de contexte transactionnel (JDBC, JTA...), mais en plus vous avez plusieurs manières de les gérer !

On distingue deux principales approches :

Dans ce cours nous verrons uniquement l'approche par programmation.

En effet, la manière déclarative impose de faire de l'AOP (Programmation Orientée Aspect). Spring fournit un module pour cela : Spring AOP (cf documentation). Mais...

Bon, après ce petit aparté, passons aux choses sérieuses...

Follow the white rabbit... direction la couche... business.

OK, maintenant que tout est clair, vous pouvez ajouter la dépendance vers Spring-TX dans le module ticket-business :

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>

I imagine that right now you're feeling a bit like Alice tumbling down the rabbit hole...

Vous avez deux solutions : la pilule bleue ou la pilule rouge...

La pilule bleue

Vous avez gouté au confort et à la simplicité avec la classe JdbcTemplate. Eh bien ça continue avec la classe TransactionTemplate !

Utiliser un TransactionTemplate
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
// import  ...

public class TicketManagerImpl extends AbstractManagerImpl implements TicketManager {

    @Inject
    @Named("txManagerTicket")
    private PlatformTransactionManager platformTransactionManager;

    @Override
    public void changerStatut(Ticket pTicket, TicketStatut pNewStatut,
                              Utilisateur pUtilisateur, Commentaire pCommentaire) {
        TransactionTemplate vTransactionTemplate
            = new TransactionTemplate(platformTransactionManager);

        vTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus
                                                            pTransactionStatus) {
                pTicket.setStatut(pNewStatut);
                getDaoFactory().getTicketDao().updateTicket(pTicket);
                // TODO Ajout de la ligne d'historique + commentaire ...
            }
        });
    }
}

Il ne reste plus qu'à définir le bean txManagerTicket de type PlatformTransactionManager à injecter via Spring IoC.

Définir le PlatformTransactionManager

PlatformTransactionManager est une interface (souvenez-vous, Spring TX permet de gérer différentes natures de transaction). Ici, j'ai besoin de gérer des transactions sur une DataSource JDBC. Je vais donc utiliser la classe DataSourceTransactionManager.

J'ajoute la définition du bean dans le fichier businessContext.xml :

<bean id="txManagerTicket" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSourceTicket"/>
</bean>
Silence, moteur... action !

Et c'est fini. Spring fait le reste ! En effet, lors de l'appel de la méthode vTransactionTemplate.execute(...) Spring va :

  1. ouvrir la transaction ;

  2. encapsuler dans un try...catch l'appel de la méthode doInTransactionWithoutResult(...) définie dans la classe anonyme de type TransactionCallbackWithoutResult ;

  3. valider la transaction après le bloc try...catch ;

  4. si une erreur survient, Spring va annuler la transaction dans le catch et :

    • propager l'exception s'il s'agit d'une RuntimeException ou d'une Error

    • lever une UndeclaredThrowableException avec le Throwable en cause dans les autres cas.

Demander vous-même l'annulation

En fonction de votre implémentation et des règles métier, vous aurez peut-être besoin de demander vous-même l'annulation de la transaction. Cela se fait en appelant la méthode setRollbackOnly() sur le TransactionStatus :

vTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus
                                                    pTransactionStatus) {
        TicketStatut vOldStatut = pTicket.getStatut();
        pTicket.setStatut(pNewStatut);
        try {
            getDaoFactory().getTicketDao().updateTicket(pTicket);
            // TODO Ajout de la ligne d'historique + commentaire ...
        } catch (TechnicalException vEx) {
            pTransactionStatus.setRollbackOnly();
            pTicket.setStatut(vOldStatut);
        }
    }
});
Renvoyer un objet en sortie

Si votre traitement transactionnel doit renvoyer un objet, il faut alors utiliser la classe TransactionCallback :

HistoriqueStatut vHistorique =
    vTransactionTemplate.execute(new TransactionCallback<HistoriqueStatut>() {
        @Override
        public HistoriqueStatut doInTransaction(TransactionStatus
                                                    pTransactionStatus) {
            pTicket.setStatut(pNewStatut);
            getDaoFactory().getTicketDao().updateTicket(pTicket);

            HistoriqueStatut vHistoriqueStatut = new HistoriqueStatut();
            // TODO Ajout de la ligne d'historique + commentaire ...
            return vHistoriqueStatut;
        }
    });
Partager le TransactionTemplate

Enfin, sachez que, comme la classe JdbcTemplate, la classe TransactionTemplate est threadsafe d'un point de vue de la gestion de transaction. Elle ne conserve pas d'état de la transaction en cours. Cependant, elle conserve la configuration des transactions (je vous en parle un peu plus loin). Vous pouvez donc partager l'instance de TransactionTemplate entre plusieurs méthodes, voire classes, si elles utilisent la même configuration.

La pilule rouge

On oublie le « tout enrobé » par la classe TransactionTemplate et on traite directement avec le PlatformTransactionManager.

Ici, Spring n'automatise plus — enfin, presque plus — l'ouverture, la validation et l'annulation de la transaction. C'est à vous de le faire.

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
// import  ...

public class TicketManagerImpl extends AbstractManagerImpl implements TicketManager {

    @Inject
    @Named("txManagerTicket")
    private PlatformTransactionManager platformTransactionManager;

    @Override
    public void changerStatut(Ticket pTicket, TicketStatut pNewStatut,
                              Utilisateur pUtilisateur, Commentaire pCommentaire) {

        TransactionStatus vTransactionStatus
            = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            pTicket.setStatut(pNewStatut);
            getDaoFactory().getTicketDao().updateTicket(pTicket);
            // TODO : Ajout de la ligne d'historique + commentaire ...
        } catch (Throwable vEx) {
            platformTransactionManager.rollback(vTransactionStatus);
            throw vEx;
        }
        platformTransactionManager.commit(vTransactionStatus);
    }
}

Ah, mais ce n'est que ça !? Je croyais qu'on allait faire un truc hyper compliqué !

Quand je dis Spring n'automatise presque plus, ce qu'il faut comprendre c'est que c'est à vous de « demander » l'ouverture/validation/annulation de la transaction, mais cela reste Spring qui s'occupe de faire le nécessaire.

Avec un verre d'eau

Je propose d'améliorer un peu le code précédent. En effet, le catch de Throwable est vivement déconseillé pour plusieurs — mais ce n'est pas l'objet de ce cours. Mieux vaut passer par le finally :

TransactionStatus vTransactionStatus
    = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
try {
    pTicket.setStatut(pNewStatut);
    getDaoFactory().getTicketDao().updateTicket(pTicket);
    // TODO : Ajout de la ligne d'historique + commentaire ...

    platformTransactionManager.commit(vTransactionStatus);
    vTransactionStatus = null;
} finally {
    if (vTransactionStatus != null) {
        platformTransactionManager.rollback(vTransactionStatus);
    }
}

Voici donc une implémentation correcte possible :

TransactionStatus vTransactionStatus
    = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
try {
    // le traitement transactionnel ...

    TransactionStatus vTScommit = vTransactionStatus;
    vTransactionStatus = null;
    platformTransactionManager.commit(vTScommit);
} finally {
    if (vTransactionStatus != null) {
        platformTransactionManager.rollback(vTransactionStatus);
    }
}

Il y a bien sûr d'autres moyens pour implémenter ce mécanisme (avec un boolean par exemple). Vous pouvez même créer une classe dédiée :

public class TransactionHelper {

    @Inject
    @Named("txManagerTicket")
    private PlatformTransactionManager platformTransactionManager;

    private DefaultTransactionDefinition definition = new DefaultTransactionDefinition();

    public MutableObject<TransactionStatus> beginTransaction() {
        return beginTransaction(null);
    }

    public MutableObject<TransactionStatus> beginTransaction(DefaultTransactionDefinition pDefinition) {
        DefaultTransactionDefinition vDefinition = pDefinition != null ? pDefinition : definition;
        TransactionStatus vStatus = platformTransactionManager.getTransaction(pDefinition);
        return new MutableObject<TransactionStatus>(vStatus);
    }

    public void commit(MutableObject<TransactionStatus> pStatus) {
        if (pStatus != null && pStatus.getValue() != null) {
            pStatus.setValue(null);
            platformTransactionManager.commit(pStatus.getValue());
        }
    }

    public void rollback(MutableObject<TransactionStatus> pStatus) {
        if (pStatus != null && pStatus.getValue() != null) {
            pStatus.setValue(null);
            platformTransactionManager.rollback(pStatus.getValue());
        }
    }
}

// ...

MutableObject<TransactionStatus> vStatus = transactionHelper.beginTransaction();
try {
    // le traitement transactionnel ...
    transactionHelper.commit(vStatus);
} finally {
    transactionHelper.rollback(vStatus);
}

Personnellement, je préfère la pilule rouge. Surtout avec l'implémentation que je viens de vous montrer. Et ceci, surtout pour deux raisons :

  1. Je trouve le code plus clair et lisible, pas de classe anonyme à créer et le debug en pas à pas en sera d'autant plus facile.

  2. Je peux laisser remonter mes propres exceptions même si ce ne sont pas des RuntimeException/Error. Le finally fera le rollback. Dans le cas du TransactionTemplate, aucune exception non RuntimeException/Error ne peut remonter car la méthode TransactionCallback.doInTransaction() ne déclare aucune exception.

public class TicketManagerImpl extends AbstractManagerImpl implements TicketManager {
    // ...
    public void changerStatut(Ticket pTicket, TicketStatut pNewStatut,
                              Utilisateur pUtilisateur, Commentaire pCommentaire)
                              throws FunctionalException {

        MutableObject<TransactionStatus> vStatus = transactionHelper.beginTransaction();
        try {
            // le traitement transactionnel ...
            throw new FunctionalException("...");

            transactionHelper.commit(vStatus);
        } finally {
            transactionHelper.rollback(vStatus);
        }
    }
}

La propagation du contexte transactionnel

Il y a un truc qui me chiffonne tout de même. Reprenons l'exemple de la cloture d'un ticket. Le changement de statut du ticket se fait dans la classe TicketManagerImpl et l'ajout du commentaire, devrait se faire via une méthode de CommentaireManagerImpl. Or, a priori, cette méthode gère aussi une transaction vu qu'elle est censée être appelée lorsqu'un utilisateur ajoute lui-même un commentaire dans un ticket.

Ça ne va pas poser un problème ?

Ah, ah, bien vu !

La réponse est : ça peut ne pas en poser ! Et c'est ici qu'intervient la notion de propagation du contexte transactionnel.

En fait, Spring fait une différence entre une transaction physique (celle du SGBD par exemple) et une transaction logique (celle que vous ouvrez avec PlatformTransactionManager.getTransaction(...)).

Quand je parle de propagation du contexte transactionnel je parle de comment Spring gère le lien entre les transactions logiques et les transactions physiques.

Spring propose plusieurs types de propagation, que vous pouvez retrouver dans l'interface TransactionDefinition :

  • PROPAGATION_MANDATORY

  • PROPAGATION_NESTED

  • PROPAGATION_NEVER

  • PROPAGATION_NOT_SUPPORTED

  • PROPAGATION_REQUIRED

  • PROPAGATION_REQUIRES_NEW

  • PROPAGATION_SUPPORTS

Je ne vais pas tous les détailler ici, je vous renvoie à la JavaDoc pour connaître le détail de chacun. Je vais cependant vous détailler les deux principaux : PROPAGATION_REQUIRED et PROPAGATION_REQUIRES_NEW.

PROPAGATION_REQUIRED

Le type PROPAGATION_REQUIRED est le type de propagation par défaut.

Quand vous demandez l'ouverture d'une transaction (PlatformTransactionManager.getTransaction(...)) dans ce mode, si une transaction physique est déjà en cours, alors elle est utilisée. Sinon, une nouvelle transaction est ouverte.

Transactions en PROPAGATION_REQUIRED (commit)

Si votre demande d'ouverture déclenche l'ouverture d'une transaction physique, alors votre transaction logique devient la transaction principale. Si en revanche vous réutilisez une transaction physique existante, alors votre transaction logique devient une sous-transaction.

Voici les règles de validation/annulation qui s'appliquent dans un sous-transaction :

  • Si vous appelez la méthode commit(), cela n'a aucun effet.

  • Si vous appelez la méthode rollback(), cela marque la transaction principale en rollback only. Si vous appelez alors commit() dans la transaction principale, cela va déclencher un rollback et lever une UnexpectedRollbackException.

Transactions en PROPAGATION_REQUIRED (rollback)

PROPAGATION_REQUIRES_NEW

Avec le type PROPAGATION_REQUIRES_NEW, quand vous demandez l'ouverture d'une transaction, une nouvelle transaction physique est toujours créée, peu importe si une transaction physique existe déjà ou pas.

Transactions en PROPAGATION_REQUIRES_NEW (commit)

Les transactions physiques (et logiques du coup), sont totalement indépendantes. L'annulation de l'une n'affecte aucunement l'autre.

Transactions en PROPAGATION_REQUIRES_NEW (rollback)
Transactions en PROPAGATION_REQUIRES_NEW (rollback)

OK, je vous ai parlé des types de propagation des transactions. Mais comment le définir ? Eh bien, comme n'importe quel autres paramètres des transactions. C'est ce que je vous montre dans la section suivante.

Ajuster les paramètres des transactions

Le détail des paramètres définissables et leur valeurs par défaut se trouve dans la documentation officielle et la JavaDoc.

La définition des paramètres des transactions se fait différement en fonction de la couleur de la pilule...

Si vous utilisez le TransactionTemplate

Si vous utilisez le TransactionTemplate, la définition des paramètres se fait directement via les setters du TransactionTemplate avant l'appel à la méthode execute(...) :

TransactionTemplate vTransactionTemplate = new TransactionTemplate();
vTransactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
vTransactionTemplate.setTimeout(30); // 30 secondes

//vTransactionTemplate.execute(...);

Si vous utilisez le PlatformTransactionManager

Si vous utilisez le PlatformTransactionManager, la définition des paramètres se fait via l'instance de DefaultTransactionDefinition passée à l'ouverture de la transaction :

DefaultTransactionDefinition vDefintion = new DefaultTransactionDefinition();
vDefintion.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
vDefintion.setTimeout(30); // 30 secondes

platformTransactionManager.getTransaction(vDefintion);

Synchroniser les transactions hors de Spring JDBC

Si vous utilisez Spring JDBC pour exécuter vos requêtes SQL, comme je le disais au début de ce chapitre, les modules fonctionnent parfaitement ensemble. La gestion des transactions au niveau de Spring JDBC est transparente pour le développeur.

En revanche, si des accès sont fait directement avec l'API JDBC (par un framework de test par exemple), alors ce dernier doit être intégré dans la mécanique transactionnelle.

Pour cela, Spring fournit deux classes utilitaires.

La classe DataSourceUtils permet d'obtenir une Connection. Spring crée un proxy au niveau de la connexion.

Connection vConnection = DataSourceUtils.getConnection(dataSourceTicket);

C'est la méthode recommandée. Cependant il arrive que n'ayez même pas besoin d'une Connexion mais directement d'une instance de DataSource. Dans ce cas, n'utilisez pas la DataSource de base (le bean dataSourceTicket dans notre application). Sans quoi, vous seriez totalement en dehors du mécanisme transactionnel de Spring. La solution est d'obtenir un proxy de la DataSource grâce à la classe TransactionAwareDataSourceProxy :

DataSource vDataSource = new TransactionAwareDataSourceProxy(dataSourceTicket);

Si besoin, vous trouverez plus de détail dans la documentation officielle.

Conclusion

La gestion des transactions avec Spring peut vous paraître au premier abord un peu verbeuse et répétitive, surtout avec la définition des paramètres de ces transactions. Mais avec ce que vous avez appris tout au long de ce cours (notamment en première partie), vous devriez être en mesure de simplifier tout cela avec l'injection de dépendances, les classes mères abstraites...

Ensuite, à vous de faire votre choix entre TransactionTemplate et PlatformTransactionManager.

La gestion des transactions est un sujet vaste et les possibilités offertes par Spring sont assez importantes. N'hésitez pas à parcourir la documentation officielle pour approfondir le sujet.

Exemple de certificat de réussite
Exemple de certificat de réussite