web-dev-qa-db-fra.com

Pourquoi ne devrions-nous pas créer un contrôleur Spring MVC @Transactional?

Il y a déjà quelques questions sur le sujet, mais aucune réponse ne fournit vraiment d'arguments pour expliquer pourquoi nous ne devrions pas créer un contrôleur Spring MVC Transactional. Voir:

Alors pourquoi?

  • Existe-t-il des problèmes techniques insurmontables ?
  • Y a-t-il des problèmes architecturaux?
  • Y a-t-il des problèmes de performances/blocage/simultanéité?
  • Faut-il parfois plusieurs transactions distinctes? Si oui, quels sont les cas d'utilisation? (J'aime la conception simplificatrice, que les appels au serveur réussissent ou échouent complètement. Cela semble être un comportement très stable)

Contexte: J'ai travaillé il y a quelques années dans une équipe sur un assez grand ERP logiciel implémenté dans C #/NHibernate/Spring.Net. L'aller-retour vers le serveur a été exactement implémenté comme ça: le transaction a été ouverte avant d'entrer dans une logique de contrôleur et a été validée ou annulée après avoir quitté le contrôleur. La transaction a été gérée dans le cadre de sorte que personne n'ait à s'en soucier. C'était une solution brillante: stable, simple, seulement quelques architectes ont dû se soucier des problèmes de transaction, le reste de l'équipe vient d'implémenter des fonctionnalités.

De mon point de vue, c'est le meilleur design que j'aie jamais vu. Alors que j'essayais de reproduire le même design avec Spring MVC, je suis entré dans un cauchemar avec des problèmes de chargement paresseux et de transaction et à chaque fois la même réponse: ne rendez pas le contrôleur transactionnel, mais pourquoi?

Merci d'avance pour vos réponses fondées!

58
jeromerg

TLDR : c'est parce que seule la couche de service dans l'application a la logique nécessaire pour identifier la portée d'une base de données/transaction commerciale. Le contrôleur et la couche de persistance par conception ne peuvent pas/ne devraient pas connaître la portée d'une transaction.

Le contrôleur peut être fait @Transactional, mais c'est en fait une recommandation courante de ne rendre la couche de service que transactionnelle (la couche de persistance ne doit pas non plus être transactionnelle).

La raison en est non pas la faisabilité technique, mais la séparation des préoccupations. La responsabilité du contrôleur est d'obtenir les demandes de paramètres, puis d'appeler une ou plusieurs méthodes de service et de combiner les résultats dans une réponse qui est ensuite renvoyée au client.

Le contrôleur a donc une fonction de coordinateur de l'exécution de la demande et de transformateur des données de domaine dans un format que le client peut consommer, comme les DTO.

La logique métier réside sur la couche de service, et la couche de persistance récupère/stocke simplement les données dans les deux sens à partir de la base de données.

La portée d'une transaction de base de données est vraiment un concept commercial autant qu'un concept technique: dans un transfert de compte, un compte ne peut être débité que si l'autre est crédité, etc., donc seule la couche de service qui contient la logique métier peut vraiment connaître le portée d'une opération de transfert de compte bancaire.

La couche de persistance ne peut pas savoir dans quelle transaction elle se trouve, prenez par exemple une méthode customerDao.saveAddress. Doit-il toujours s'exécuter dans sa propre transaction distincte? il n'y a aucun moyen de le savoir, cela dépend de la logique métier qui l'appelle. Parfois, il doit s'exécuter sur une transaction distincte, parfois enregistrer ses données uniquement si le saveCustomer a également fonctionné, etc.

La même chose s'applique au contrôleur: saveCustomer et saveErrorMessages doivent-ils aller dans la même transaction? Vous voudrez peut-être enregistrer le client et si cela échoue, essayez d'enregistrer certains messages d'erreur et de renvoyer un message d'erreur approprié au client, au lieu de tout restaurer, y compris les messages d'erreur que vous vouliez enregistrer dans la base de données.

Dans les contrôleurs non transactionnels, les méthodes renvoyant de la couche de service retournent des entités détachées car la session est fermée. Ceci est normal, la solution consiste à utiliser OpenSessionInViewou à effectuer des requêtes qui souhaitent récupérer les résultats dont le contrôleur sait qu'il a besoin.

Cela dit, ce n'est pas un crime de rendre les contrôleurs transactionnels, ce n'est tout simplement pas la pratique la plus fréquemment utilisée.

97
Angular University

J'ai vu les deux cas dans la pratique, dans des applications Web d'entreprise de moyenne à grande taille, utilisant divers cadres Web (JSP/Struts 1.x, GWT, JSF 2, avec Java EE et Spring) ).

D'après mon expérience, il est préférable de délimiter les transactions au plus haut niveau, c'est-à-dire au niveau du "contrôleur".

Dans un cas, nous avions une classe BaseAction étendant la classe Action de Struts, avec une implémentation de la méthode execute(...) qui gérait la gestion de session Hibernate (enregistrée dans un ThreadLocal object), transaction begin/commit/rollback et le mappage des exceptions aux messages d'erreur conviviaux. Cette méthode annulerait simplement la transaction en cours si une exception se propageait jusqu'à ce niveau ou si elle était marquée pour la restauration uniquement; sinon, il validerait la transaction. Cela a fonctionné dans tous les cas, où normalement il y a une seule transaction de base de données pour l'ensemble du cycle de requête/réponse HTTP. Les cas rares où plusieurs transactions étaient nécessaires seraient traités dans un code spécifique au cas d'utilisation.

Dans le cas de GWT-RPC, une solution similaire a été implémentée par une implémentation de base de servlet GWT.

Avec JSF 2, je n'ai jusqu'à présent utilisé que la démarcation au niveau du service (en utilisant des beans de session EJB qui ont automatiquement une propagation de transaction "REQUISE"). Il y a des inconvénients ici, par opposition à la délimitation des transactions au niveau des beans de sauvegarde JSF. Fondamentalement, le problème est que, dans de nombreux cas, le contrôleur JSF doit effectuer plusieurs appels de service, chacun accédant à la base de données d'application. Avec les transactions au niveau du service, cela implique plusieurs transactions distinctes (toutes validées, sauf exception), ce qui alourdit davantage le serveur de base de données. Ce n'est pas seulement un inconvénient en termes de performances. Avoir plusieurs transactions pour une seule demande/réponse peut également conduire à des bugs subtils (je ne me souviens plus des détails, juste que de tels problèmes se sont produits).

Une autre réponse à cette question parle de "la logique nécessaire pour identifier la portée d'une base de données/transaction commerciale". Cet argument n'a pas de sens pour moi, car il n'y a aucune logique associée à la démarcation des transactions, normalement. Ni les classes de contrôleur ni les classes de service ne doivent réellement "connaître" les transactions. Dans la grande majorité des cas, dans une application Web, chaque opération commerciale se produit à l'intérieur d'une paire requête/réponse HTTP, la portée de la transaction étant toutes les opérations individuelles exécutées depuis le moment où la demande est reçue jusqu'à la fin de la réponse.

Parfois, un service commercial ou un contrôleur peut avoir besoin de gérer une exception d'une manière particulière, puis probablement marquer la transaction en cours pour l'annulation uniquement. Dans Java EE (JTA), cela se fait en appelant serTransaction # setRollbackOnly () . L'objet UserTransaction peut être injecté dans un @Resource, Ou obtenu par programmation à partir de quelque ThreadLocal. Au printemps, l'annotation @Transactional Permet de spécifier la restauration pour certains types d'exceptions, ou le code peut obtenir un thread local TransactionStatus et appelez setRollbackOnly().

Donc, à mon avis et selon mon expérience, rendre le contrôleur transactionnel est la meilleure approche.

17
Rogério

Parfois, vous souhaitez annuler une transaction lorsqu'une exception est levée, mais en même temps vous souhaitez gérer l'exception, créez une réponse appropriée dans le contrôleur.

Si vous mettez @Transactional sur la méthode du contrôleur, la seule façon d'imposer la restauration pour lancer la transaction à partir de la méthode du contrôleur, mais vous ne pouvez pas retourner un objet de réponse normal.

Mise à jour: Un retour en arrière peut également être réalisé par programme, comme indiqué dans Réponse de Rodério .

Une meilleure solution consiste à rendre votre méthode de service transactionnelle, puis à gérer une éventuelle exception dans les méthodes du contrôleur.

L'exemple suivant montre un service utilisateur avec une méthode createUser, cette méthode est chargée de créer l'utilisateur et d'envoyer un e-mail à l'utilisateur. Si l'envoi du mail échoue, nous voulons annuler la création de l'utilisateur:

@Service
public class UserService {

    @Transactional
    public User createUser(Dto userDetails) {

        // 1. create user and persist to DB

        // 2. submit a confirmation mail
        //    -> might cause exception if mail server has an error

        // return the user
    }
}

Ensuite, dans votre contrôleur, vous pouvez encapsuler l'appel à createUser dans un try/catch et créer une réponse appropriée à l'utilisateur:

@Controller
public class UserController {

    @RequestMapping
    public UserResultDto createUser (UserDto userDto) {

        UserResultDto result = new UserResultDto();

        try {

            User user = userService.createUser(userDto);

            // built result from user

        } catch (Exception e) {
            // transaction has already been rolled back.

            result.message = "User could not be created " + 
                             "because mail server caused error";
        }

        return result;
    }
}

Si vous mettez un @Transaction sur votre méthode de contrôleur qui n'est tout simplement pas possible.

6
lanoxx