it-swarm.com.de

Warum sollten wir keinen Spring MVC Controller @Transactional bauen?

Es gibt bereits einige Fragen zu diesem Thema, aber keine Antwort liefert wirklich Argumente, um zu erklären, warum wir keinen Spring MVC-Controller Transactional erstellen sollten. Sehen:

Warum also?

  • Gibt es unüberwindbare technische Probleme?
  • Gibt es architektonische Probleme?
  • Gibt es Performance-/Deadlock-/Concurrency-Probleme?
  • Sind manchmal mehrere separate Transaktionen erforderlich? Wenn ja, welche Anwendungsfälle gibt es? (Ich mag das vereinfachende Design, dass Aufrufe an den Server entweder vollständig erfolgreich sind oder vollständig fehlschlagen. Es scheint ein sehr stabiles Verhalten zu sein.)

Hintergrund: Ich habe vor einigen Jahren in einem Team an einer ziemlich großen ERP Software gearbeitet, die in C #/NHibernate/Spring.Net implementiert wurde. Der Roundtrip zum Server wurde genau so implementiert: Die Transaktion wurde vor dem Eingeben einer Controller-Logik geöffnet und nach dem Beenden des Controllers festgeschrieben oder rückgängig gemacht. Die Transaktion wurde im Framework verwaltet, sodass sich niemand darum kümmern musste. Es war eine brillante Lösung: stabil, einfach, nur wenige Architekten mussten sich um Transaktionsprobleme kümmern, der Rest des Teams implementierte gerade Funktionen.

Aus meiner Sicht ist es das beste Design, das ich je gesehen habe. Als ich versuchte, dasselbe Design mit Spring MVC zu reproduzieren, geriet ich in einen Albtraum mit Problemen beim verzögerten Laden und bei Transaktionen und jedes Mal mit der gleichen Antwort: Machen Sie den Controller nicht transaktional, aber warum?

Vielen Dank im Voraus für Ihre fundierten Antworten!

58
jeromerg

TLDR : Dies liegt daran, dass nur die Serviceschicht in der Anwendung über die Logik verfügt, die zur Identifizierung des Umfangs einer Datenbank/eines Geschäftsvorgangs erforderlich ist. Der Controller und die Persistenzschicht können bzw. sollten den Umfang einer Transaktion nicht kennen.

Der Controller kann hergestellt werden @Transactional, Aber es ist in der Tat eine übliche Empfehlung, nur die Service-Schicht transaktional zu machen (die Persistenzschicht sollte auch nicht transaktional sein).

Der Grund dafür ist nicht die technische Machbarkeit, sondern die Trennung von Bedenken. Die Verantwortung des Controllers besteht darin, die Parameteranforderungen abzurufen, eine oder mehrere Dienstmethoden aufzurufen und die Ergebnisse in einer Antwort zu kombinieren, die dann an den Client zurückgesendet wird.

Der Controller hat also die Funktion, die Ausführung der Anforderung zu koordinieren und die Domänendaten in ein Format umzuwandeln, das der Client verwenden kann, z. B. DTOs.

Die Geschäftslogik befindet sich auf der Serviceschicht, und die Persistenzschicht ruft nur Daten von der Datenbank ab bzw. speichert sie.

Der Umfang einer Datenbanktransaktion ist sowohl ein Geschäftskonzept als auch ein technisches Konzept: Bei einer Kontoübertragung kann ein Konto nur belastet werden, wenn das andere gutgeschrieben wird usw., sodass nur die Serviceschicht, die die Geschäftslogik enthält, das wirklich wissen kann Umfang einer Überweisung auf ein Bankkonto.

Die Persistenzschicht kann nicht wissen, in welcher Transaktion sie sich befindet. Nehmen Sie zum Beispiel eine Methode customerDao.saveAddress. Sollte es immer in einer eigenen Transaktion laufen? Es gibt keine Möglichkeit zu wissen, es hängt von der Geschäftslogik ab, die es aufruft. Manchmal sollte es in einer separaten Transaktion ausgeführt werden, manchmal sollten die Daten nur gespeichert werden, wenn auch saveCustomer funktioniert hat usw.

Gleiches gilt für den Controller: Sollen saveCustomer und saveErrorMessages in dieselbe Transaktion gehen? Möglicherweise möchten Sie den Kunden speichern. Wenn dies fehlschlägt, versuchen Sie, einige Fehlermeldungen zu speichern und eine richtige Fehlermeldung an den Client zurückzugeben, anstatt alles zurückzusetzen, einschließlich der Fehlermeldungen, die Sie in der Datenbank speichern möchten.

In Nicht-Transaktions-Controllern geben Methoden, die von der Service-Schicht zurückkehren, getrennte Entitäten zurück, da die Sitzung geschlossen ist. Dies ist normal. Die Lösung besteht darin, entweder OpenSessionInViewzu verwenden oder Abfragen durchzuführen, die eifrig die Ergebnisse abrufen, von denen der Controller weiß, dass sie benötigt werden.

Trotzdem ist es kein Verbrechen, Controller zu Transaktionszwecken zu verwenden, sondern nur nicht die am häufigsten angewandte Praxis.

97

Ich habe beide Fälle in der Praxis bei mittelgroßen bis großen Business-Webanwendungen mit verschiedenen Web-Frameworks (JSP/Struts 1.x, GWT, JSF 2, mit Java EE und Spring) gesehen ).

Nach meiner Erfahrung ist es am besten, Transaktionen auf der höchsten Ebene, dh auf der Ebene des "Controllers", abzugrenzen.

In einem Fall hatten wir eine BaseAction -Klasse, die die Action -Klasse von Struts erweiterte, mit einer Implementierung für die execute(...) -Methode, die das Hibernate-Sitzungsmanagement handhabte (in einem ThreadLocal object), Transaktion begin/commit/rollback und die Zuordnung von Ausnahmen zu benutzerfreundlichen Fehlermeldungen. Bei dieser Methode wird die aktuelle Transaktion einfach zurückgesetzt, wenn eine Ausnahme bis zu dieser Ebene propagiert wurde oder wenn sie nur für das Zurücksetzen markiert wurde. Andernfalls würde die Transaktion festgeschrieben. Dies funktionierte in allen Fällen, in denen normalerweise eine einzige Datenbanktransaktion für den gesamten HTTP-Anforderungs-/Antwortzyklus vorhanden ist. In seltenen Fällen, in denen mehrere Transaktionen erforderlich waren, wurde ein anwendungsfallspezifischer Code verwendet.

Im Fall von GWT-RPC wurde eine ähnliche Lösung durch eine Basis-GWT-Servlet-Implementierung implementiert.

Mit JSF 2 habe ich bisher nur die Service-Level-Abgrenzung verwendet (mit EJB-Session-Beans, die automatisch die Transaktionspropagierung "ERFORDERLICH" haben). Hier gibt es Nachteile gegenüber der Abgrenzung von Transaktionen auf der Ebene der JSF-Backing-Beans. Grundsätzlich besteht das Problem darin, dass der JSF-Controller in vielen Fällen mehrere Serviceaufrufe ausführen muss, von denen jeder auf die Anwendungsdatenbank zugreift. Bei Transaktionen auf Service-Ebene bedeutet dies mehrere separate Transaktionen (alle festgeschrieben, sofern keine Ausnahme vorliegt), die den Datenbankserver stärker belasten. Dies ist jedoch nicht nur ein Leistungsnachteil. Das Vorhandensein mehrerer Transaktionen für eine einzelne Anfrage/Antwort kann auch zu subtilen Fehlern führen (ich kann mich nicht mehr an die Details erinnern, nur dass solche Probleme aufgetreten sind).

Eine andere Antwort auf diese Frage spricht von "Logik, die benötigt wird, um den Umfang einer Datenbank/eines Geschäftsvorgangs zu identifizieren". Dieses Argument macht für mich keinen Sinn, da normalerweise keine Logik mit der Transaktionsabgrenzung verbunden ist. Weder Controller-Klassen noch Service-Klassen müssen tatsächlich über Transaktionen Bescheid wissen. In den allermeisten Fällen findet in einer Web-App jeder Geschäftsvorgang innerhalb eines HTTP-Anfrage/Antwort-Paares statt, wobei der Umfang der Transaktion alle einzelnen Vorgänge umfasst, die von dem Zeitpunkt an ausgeführt werden, an dem die Anfrage eingeht, bis die Antwort abgeschlossen ist.

Gelegentlich muss ein Geschäftsservice oder ein Controller möglicherweise eine Ausnahme auf eine bestimmte Weise behandeln und dann die aktuelle Transaktion wahrscheinlich nur für das Rollback markieren. In Java EE (JTA)) erfolgt dies durch Aufrufen von serTransaction # setRollbackOnly () . Das UserTransaction -Objekt kann in ein @Resource -Feld oder programmgesteuert von einem ThreadLocal abgerufen. Im Frühjahr ermöglicht die Annotation @Transactional, Dass für bestimmte Ausnahmetypen ein Rollback angegeben wird, oder Code kann einen threadlokalen TransactionStatus und setRollbackOnly() aufrufen.

Meiner Meinung nach und meiner Erfahrung nach ist es daher besser, den Controller transaktional zu machen.

17
Rogério

Manchmal möchten Sie eine Transaktion zurücksetzen, wenn eine Ausnahme ausgelöst wird, gleichzeitig möchten Sie jedoch die Ausnahme behandeln und im Controller eine ordnungsgemäße Antwort darauf erstellen.

Wenn Sie @Transactional auf der Controllermethode die einzige Möglichkeit, das Rollback zu erzwingen, um die Transaktion von der Controllermethode auszulösen, aber Sie können dann kein normales Antwortobjekt zurückgeben.

Update: Ein Rollback kann auch programmgesteuert durchgeführt werden, wie in Rodérios Antwort beschrieben.

Eine bessere Lösung besteht darin, Ihre Dienstmethode transaktional zu machen und dann eine mögliche Ausnahme in den Controller-Methoden zu behandeln.

Das folgende Beispiel zeigt einen Benutzerdienst mit einer createUser -Methode. Diese Methode ist dafür verantwortlich, den Benutzer zu erstellen und eine E-Mail an den Benutzer zu senden. Wenn das Senden der E-Mail fehlschlägt, möchten wir die Benutzererstellung zurücksetzen:

@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
    }
}

Dann können Sie in Ihrem Controller den Aufruf von createUser in ein try/catch-Objekt einschließen und dem Benutzer eine richtige Antwort geben:

@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;
    }
}

Wenn Sie ein @Transaction auf Ihrer Controller-Methode ist das einfach nicht möglich.

6
lanoxx