Pourquoi ne pas faire un contrôleur MVC printemps @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 faire un contrôleur MVC de printemps Transactional . Voir:

alors, pourquoi?

  • Est-il insurmontable problèmes techniques?
  • y a-t-il des problèmes d'architecture?
  • est là questions de rendement/impasse / concurrence?
  • faut-il parfois effectuer plusieurs transactions distinctes? Si oui, quels sont les cas d'utilisation? (J'aime la simplification de la conception, que les appels vers le serveur soit complètement réussir ou échouer. Il semble être un très un comportement stable)

arrière-plan: J'ai travaillé il y a quelques années dans une équipe sur un logiciel ERP assez grand mis en œuvre dans C#/NHibernate/Spring.Net. L'aller-retour vers le serveur était exactement mise en œuvre comme cela: la transaction a été ouverte avant d'entrer dans la logique du contrôleur et a été engagée ou volée après avoir quitté le contrôleur. L'opération a été géré dans le cadre, de sorte que personne n'ait à s'en soucier. c'était une solution brillante: stable, simple, seuls quelques architectes ont dû se soucier des problèmes de transaction, le reste de l'équipe vient de mettre en place des fonctionnalités.

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

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

51
demandé sur Community 2014-04-16 23:46:12

3 réponses

TLDR : ceci est dû au fait que seule la couche service de l'application possède 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 en fait c'est une recommandation courante de ne faire de la couche service qu'une couche transactionnelle (la couche persistance ne devrait pas être transactionnelle non plus).

la raison de cela n'est 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 envoyée au client.

de sorte que le contrôleur a une fonction de coordinateur de l'exécution de la requête, et de transformer les données du domaine dans un format que le client peut consommer tel que DTOs.

la logique commerciale réside dans la couche service, et la couche persistance ne fait que récupérer / stocker des données de part et d'autre de la base de données.

la portée d'une transaction de base de données est en réalité un concept d'entreprise 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 d'affaires peut vraiment connaître la portée d'une transaction de transfert de compte bancaire.

la couche de persistance ne peut pas savoir dans quelle transaction elle est, Prenez par exemple une méthode customerDao.saveAddress . Devrait-il fonctionner dans son propre opération? il n'y a aucun moyen de savoir, il dépend de la logique métier en l'appelant. Parfois, il doit être exécuté sur une transaction séparée, parfois seulement enregistrer ses données si le saveCustomer a également fonctionné, etc.

il en va de même pour le contrôleur: saveCustomer et saveErrorMessages doivent-ils faire l'objet de la même transaction? Vous pourriez vous voulez sauver le client et si cela échoue alors essayer de sauver quelques messages d'erreur et de retourner un message d'erreur approprié au client, au lieu de revenir en arrière tout y compris les messages d'erreur que vous vouliez sauver sur la base de données.

dans les contrôleurs non transactionnels, les méthodes revenant de la couche service renvoient les entités détachées parce que la session est fermée. C'est normal, la solution est soit d'utiliser OpenSessionInView ou de faire des requêtes qui cherchent les résultats 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 utilisée.

79
répondu Angular University 2016-07-23 14:45:00

j'ai vu les deux cas dans la pratique, dans des applications web de moyenne ou grande taille, en 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 niveau le plus élevé, c'est-à-dire au niveau du" contrôleur".

dans un cas, nous avons eu une classe BaseAction rallongeant les entretoises Action , avec une implémentation pour la méthode execute(...) qui manipulait La gestion des sessions d'hibernation (sauvegardée dans un objet ThreadLocal ), les transactions begin/commit/rollback, et la mise en correspondance des exceptions avec les messages d'erreur conviviaux. Cette méthode consiste simplement à annuler la transaction en cours Si une exception est propagée jusqu'à ce niveau, ou si elle est marquée uniquement pour l'annulation; sinon, elle déclenche la transaction. Cela a fonctionné dans tous les cas, où il y a normalement une seule transaction de base de données pour l'ensemble du cycle de requête/réponse HTTP. Les rares cas où plusieurs transactions étaient nécessaires et seraient traitées dans un code propre au cas d'utilisation.

dans le cas de GWT-RPC, une solution similaire a été mise en œuvre par une implémentation de Servlet GWT de base.

avec JSF 2, Je n'ai utilisé jusqu'à présent que la démarcation du niveau de service (en utilisant des haricots de Session EJB qui ont automatiquement "requis" la propagation de la transaction). Il y a ici des inconvénients, par opposition à la délimitation des transactions au niveau du JSF support beans. Fondamentalement, le problème est que dans de nombreux cas, le contrôleur JSF doit faire plusieurs appels de service, chacun accédant à la base de données de l'application. Pour les transactions au niveau de service, cela implique plusieurs transactions distinctes (toutes engagées, sauf exception), qui taxent davantage le serveur de la base de données. Ce n'est pas seulement un inconvénient de performance, cependant. Avoir plusieurs transactions pour une seule requête / réponse peut aussi conduire à des bugs subtils (Je ne me souviens plus des détails, juste que de tels problèmes ont fait produire.)

autre réponse à cette question parle de"logique nécessaire pour identifier la portée d'une base de données/transaction d'affaires". Cet argument n'a pas de sens pour moi, puisqu'il y a Non logique associée à la démarcation de transaction du tout, normalement. Ni les classes de contrôleur ni les classes de service n'ont besoin de 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 requête/réponse HTTP paire, avec la portée de la transaction étant toutes les opérations individuelles étant exécutées à partir du point de la demande est reçue jusqu'à ce que la réponse soit terminée.

à l'occasion, un service d'affaires ou un contrôleur peut avoir besoin de traiter une exception d'une manière particulière, alors marquez probablement la transaction actuelle pour le retrait seulement. En Java EE (JTA), ceci est fait en appelant UserTransaction#setRollbackOnly () . L'objet UserTransaction peut être injecté dans un champ @Resource , ou obtenu par programmation À partir d'un certain ThreadLocal . Au printemps, l'annotation @Transactional permet de spécifier le rollback pour certains types d'exception, ou le code peut obtenir un thread-local TransactionStatus et appeler setRollbackOnly() .

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

10
répondu Rogério 2017-06-02 15:18:39

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

si vous mettez @Transactional sur la méthode controller la seule façon de forcer le roll-back pour lancer la transaction à partir de la méthode controller, mais alors vous ne pouvez pas retourner un objet de réponse normale.

mise à Jour: un rollback peut également être réalisé par programmation, comme indiqué dans Rodério's answer .

une meilleure solution est de rendre votre méthode de service transactionnel et ensuite gérer une exception possible dans les méthodes de contrôleur.

l'exemple suivant montre un service utilisateur avec une méthode createUser , cette méthode est responsable de créer l'utilisateur et d'envoyer un e-mail à l'utilisateur. Si l'envoi échoue, nous voulons rollback 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
    }
}

alors dans votre controller vous pouvez envelopper 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 controller ce n'est tout simplement pas possible.

4
répondu lanoxx 2017-06-05 13:53:29