CodeGym /Kurse /Hibernate deep-dive /Transaktionsgrenzen im Service

Transaktionsgrenzen im Service

Hibernate deep-dive
Level 17, Lektion 1
Verfügbar

1. Die Transaktion als Grenze einer Operation

Der Runtime-Zyklus von Hibernate ist bereits sichtbar: ein Persistenzkontext, Dirty Checking, flush, commit, rollback. Nun stellt sich die wichtigste praktische Frage – wo diese Grenze in der Anwendung überhaupt lebt. Wenn sie nicht mit einer abgeschlossenen Geschäftsoperation übereinstimmt, zerfällt dieser ganze schöne Mechanismus in detached-Objekte, überflüssige save-Aufrufe und zufällige merge-Operationen.

Um dies ohne unnötiges Rauschen zu sehen, nehmen wir ein vereinfachtes Order/Inventory-Szenario: „Bestellung bestätigen und Restbestand reservieren“. Dies sind nicht zwei Operationen, „weil es zwei Repositories gibt“, sondern eine Operation, weil sie einen einzigen Sinn und ein einziges erwartetes Ergebnis hat: entweder ist die Bestellung bestätigt und der Bestand reserviert, oder nichts davon ist passiert. An dieser Stelle fällt normalerweise das Wort „Atomarität“, aber ich übersetze es in menschliche Sprache: Atomarität bedeutet „alles oder nichts“, nicht „nun, die Hälfte haben wir gespeichert, mal sehen, was weiter passiert“.

Hibernate ist genau darauf ausgelegt, einen einzelnen Unit of Work gut zu bedienen: Innerhalb der Grenze lesen wir Daten, prüfen Bedingungen, ändern managed-Entitäten, und dann führt Hibernate von selbst die nötigen UPDATE-Anweisungen bei flush/commit durch. Aber das funktioniert nur, wenn wir tatsächlich eine Grenze haben, innerhalb derer ein einziger Persistenzkontext lebt. Und genau hier kommt der Hauptdarsteller der heutigen Vorlesung ins Spiel: Die Service-Methode als richtiger Ort für die Transaktionsgrenze.

2. Rollen von Repository und Service

In der frühen Lernphase scheint es, dass Repository und Service sich nur dadurch unterscheiden, dass „es in Spring so üblich ist“. Deshalb entsteht die Versuchung: „Lass uns einfach alles im Repository machen“ oder „Lass uns die Transaktion dorthin setzen, wo es uns passt“. Aber diese Schichten unterscheiden sich nicht im Stil, sondern in der Bedeutung: Sie beantworten unterschiedliche Fragen.

Das Repository beantwortet die Frage „Wie hole/speichere ich Daten?“. Es kann gut findById, save, delete, manchmal ausdrucksstarke Query-Methoden und Leseabfragen. Ein Repository ist ein Datenzugriffsschritt. Es sollte normalerweise nicht wissen, was „Bestellbestätigung“ oder „Bestandsreservierung“ als Geschäftsereignis bedeutet. Es kann wissen, wie man InventoryItem anhand der productId findet, aber es sollte nicht entscheiden, in welcher Reihenfolge der Bestellstatus und die Lagerbestände geändert werden.

Der Service beantwortet die Frage „Wie führe ich die gesamte Geschäftsoperation aus?“. Das heißt, der Service ist der Ort, an dem wir das Szenario zusammenstellen: die benötigten Entitäten laden, Invarianten prüfen, den Zustand ändern, die Operation mit einem vorhersagbaren Ergebnis abschließen. Und genau deshalb ist die Serviceschicht der natürliche Ort für die Transaktionsgrenze: weil die Transaktion mit der Grenze eines einzelnen Szenarios zusammenfallen sollte, und das Szenario lebt im Service.

Es ist praktisch, dies als kleine Tabelle im Kopf zu behalten (ohne Dogmatismus, nur als Orientierungshilfe):

Frage Normalerweise antwortet Repository Normalerweise antwortet Service
„Wo ist PurchaseOrder mit dieser ID?“ Ja Nein
„Wie bestätige ich eine Bestellung korrekt?“ Nein Ja
„Muss InventoryItem zusammen mit der Bestellung geändert werden?“ Nein Ja
„Welches SQL wird bei Änderung ausgeführt?“ Indirekt Bewusst (durch Szenario + SQL-Log)
„Wo soll die Transaktion sein?“ Manchmal (standardmäßig) Ja, als gemeinsame Grenze des Unit of Work

Beachten Sie das „Manchmal (standardmäßig)“ beim Repository. Das ist eine wichtige Nuance, die oft in die Irre führt: Spring Data JPA fügt den Repository-Methoden tatsächlich Transaktionalität hinzu, aber das bedeutet nicht, dass Sie automatisch die transaktionale Integrität einer Geschäftsoperation erhalten. Das werden wir jetzt genauer betrachten.

Und noch eine Voraussetzung für die folgenden Service-Beispiele: Wir sprechen über einen normalen externen Aufruf einer public-Methode eines Spring-Beans. Wenn der Aufruf am Proxy vorbei erfolgt, wird die @Transactional-Grenze möglicherweise nicht aktiviert, und dann funktioniert das ganze Modell einfach nicht.

3. Transaktionen in Repositories: Was schiefgeht

Von außen erscheint es logisch: „Das Repository ist ja bereits transaktional, also ist alles in Ordnung.“ Besonders wenn der Code kurz ist und die Tests grün sind. Das Problem ist, dass die standardmäßige Transaktionalität des Repositories die Transaktionalität jedes einzelnen Aufrufs ist, nicht der gesamten Operation.

Betrachten wir die „schlechte“ Variante, die man aus Gewohnheit sehr leicht schreibt:

import org.springframework.stereotype.Service;

@Service
public class CheckoutService {

    public void confirmOrderBad(Long orderId, Long productId, int qty) {
        // Schritt 1: Laden der Bestellung durch einen separaten Repository-Aufruf (oft in einer eigenen kleinen Tx)
        PurchaseOrder order = orderRepository.findById(orderId).orElseThrow();

        // Wichtig: Wenn keine externe @Transactional vorhanden ist, wird die Entität zu diesem Zeitpunkt leicht detached
        order.setStatus(OrderStatus.CONFIRMED);

        // Schritt 2: save() wird hier oft zu einem merge-flow (mit zusätzlichen SELECTs)
        orderRepository.save(order);

        // Schritt 3: Die gleiche Geschichte wiederholt sich für die Lagerentität
        InventoryItem item = inventoryRepository.findByProductId(productId).orElseThrow();
        item.reserve(qty);

        // Schritt 4: Zweiter merge-flow, Ergebnis ist eine „Kette von Speicherungen“, kein einziges Szenario
        inventoryRepository.save(item);
    }
}

Der Code sieht anständig aus: Bestellung gefunden, Status geändert, gespeichert; Bestand gefunden, reserviert, gespeichert. Aber wenn wir uns an unser Modell der Entitätszustände erinnern, beginnt ein interessantes Drama.

Wenn Sie orderRepository.findById(...) ohne eine externe Service-Transaktion aufrufen, erfolgt dieser Aufruf normalerweise in einer eigenen kleinen Transaktion (mindestens read-only). Das Repository führt SELECT aus, gibt Ihnen das Objekt zurück … und beendet sofort seine eigene Transaktion. In diesem Moment wird PurchaseOrder von managed zu detached, weil der Persistenzkontext bereits geschlossen ist.

Als nächstes rufen Sie order.setStatus(...) auf. In Java funktioniert das immer: Der Setter weiß nichts von Datenbanken und Transaktionen, er ändert brav das Feld. Aber Hibernate kontrolliert dieses Objekt nicht mehr als managed-Entität – Snapshot, Dirty Checking und die normale Unit-of-Work-Logik sind hier nicht mehr beteiligt. Und wenn Sie dann orderRepository.save(...) aufrufen, starten Sie im Grunde einen detached-flow, den wir bereits im Modul über merge besprochen haben: Spring Data wird sehr oft gezwungen sein, merge anzuwenden, um „den Zustand des detached-Objekts in eine managed-Kopie zu übertragen“.

Und hier treten zwei unangenehme Effekte auf. Der erste – Kosten und Unvorhersagbarkeit: merge kann zu zusätzlichen SELECT-Anweisungen führen, weil Hibernate den aktuellen Zustand in der Datenbank ermitteln und eine managed-Instanz erstellen/abrufen muss, in die die Daten kopiert werden. Der zweite – semantisch: Ihre Operation wird zu einer Kette einzelner Speicherungen, nicht zu einem einheitlichen Szenario.

Der Lagerteil (InventoryItem) wiederholt dieselbe Geschichte: findByProductId hat die Entität in einer Transaktion zurückgegeben, dann wurde sie detached, dann macht save wieder merge-Semantik.

Wenn man sich das SQL-Log ansieht (sehr grob, ohne Spaltendetails), sieht das „schlechte“ Szenario oft so aus:

-- Tx #1 (read-only des Repos): Einfach die Bestellung lesen und sofort die „Managed-Welt schließen“
SELECT ... FROM purchase_order WHERE id = ?;

-- Tx #2 (save -> merge): Die managed-Kopie neu aufbauen, um die Änderungen des detached-Objekts anzuwenden
SELECT ... FROM purchase_order WHERE id = ?;   -- um eine managed copy zu erzeugen
UPDATE purchase_order SET status = ? WHERE id = ?;

-- Tx #3 (read-only des Repos): Die Lagerentität in einer separaten Transaktion lesen
SELECT ... FROM inventory_item WHERE product_id = ?;

-- Tx #4 (save -> merge): Wieder merge-flow und Update in einer neuen Tx
SELECT ... FROM inventory_item WHERE id = ?;
UPDATE inventory_item SET available_qty = ?, reserved_qty = ? WHERE id = ?;

Man kann über Nuancen streiten, aber die allgemeine Form ist sehr wiedererkennbar: viele kurze Transaktionen, viele separate Persistenzkontexte, viele wiederholte SELECT-Anweisungen.

Fügen wir nun die Realität des Lebens hinzu: Was passiert, wenn reserve(qty) innerhalb von InventoryItem feststellt, dass der Artikel nicht vorhanden ist (oder nicht ausreicht) und eine Exception wirft? Eine sehr häufige Situation. In unserem „schlechten“ Szenario könnte orderRepository.save(order) zu diesem Zeitpunkt bereits in seiner eigenen Transaktion committet haben. Das Ergebnis: Die Bestellung ist bestätigt, aber die Reservierung wurde nicht durchgeführt. Das System befindet sich in einem Zustand, der „logisch unmöglich, aber in der Datenbank vorhanden“ ist.

Um dies auf der Ebene „wie es auseinanderfällt“ zu spüren, ist es nützlich, die Aufruffolge als mehrere unabhängige Mini-Operationen zu betrachten:

sequenceDiagram
    participant S as CheckoutService
    participant OR as OrderRepository
    participant IR as InventoryRepository
    participant DB as PostgreSQL

    S->>OR: "findById(orderId)"
    Note over OR,DB: "Tx #1 (read-only)"
    OR->>DB: SELECT order

    S->>OR: "save(order)"
    Note over OR,DB: "Tx #2 (write, merge-flow)"
    OR->>DB: "SELECT order (merge)"
    OR->>DB: UPDATE order

    S->>IR: "findByProductId(productId)"
    Note over IR,DB: "Tx #3 (read-only)"
    IR->>DB: SELECT inventory

    S->>IR: "save(item)"
    Note over IR,DB: "Tx #4 (write, merge-flow)"
    IR->>DB: "SELECT inventory (merge)"
    IR->>DB: UPDATE inventory

Das Hauptproblem ist nicht, dass „4 Transaktionen schlecht für die Performance sind“ (obwohl das auch stimmt). Das Hauptproblem ist, dass wir die Integrität der Geschäftsoperation verloren haben. Und wenn Sie dann anfangen, „seltsame“ Bugs zu jagen, werden Sie sie mit Krücken behandeln: „Lass es uns noch einmal lesen“, „Lass es uns noch einmal speichern“, „Lass es uns mit saveAndFlush versuchen“ – und am Ende erhalten Sie eine maximal verrauschte Persistenzschicht.

4. Eine Operation – eine Service-Transaktion

Nach dem vorherigen Bild möchte man eine einfache Bewegung machen: @Transactional auf den Service setzen. Und das ist in der Tat eine einfache Bewegung … mit einem unverhältnismäßig großen Effekt. Denn sie bringt uns das korrekte Runtime-Modell von Hibernate zurück: eine Operation = ein Persistenzkontext = ein Unit of Work. In diesem vereinfachten Order/Inventory-Szenario wird dies besonders deutlich sichtbar: Wir erstellen noch keine neue PurchaseOrder, sondern isolieren lediglich die Tatsache einer einzigen gemeinsamen Grenze.

Hier ist dasselbe Szenario, aber bereits „erwachsen“:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CheckoutService {

    @Transactional
    public void confirmOrder(Long orderId, Long productId, int qty) {
        // Beide Objekte werden innerhalb einer Transaktion und eines Persistenzkontexts geladen
        PurchaseOrder order = orderRepository.findById(orderId).orElseThrow();
        InventoryItem item = inventoryRepository.findByProductId(productId).orElseThrow();

        // Zustand über Domänenmethoden ändern: so liest sich das Szenario wie ein Szenario
        order.confirm();
        item.reserve(qty);

        // Wichtig: Explizite save()-Aufrufe sind für bereits geladene managed-Entitäten normalerweise nicht nötig
        // Hibernate führt die UPDATEs selbst bei flush/commit durch (dirty checking)
    }
}

Wichtig ist hier nicht, dass „wir eine Annotation hinzugefügt haben“. Wichtig ist, dass jetzt die gesamte Methode innerhalb einer Transaktionsgrenze ausgeführt wird. Das bedeutet:

Erstens werden order und item nach dem Laden zu managed-Objekten in einem einzigen Persistenzkontext. Das bedeutet, dass die Änderungen, die wir mit den Methoden confirm() und reserve() vornehmen, automatisch von Dirty Checking erfasst werden. Wir müssen nicht mechanisch nach jeder Änderung save aufrufen, weil Hibernate die Änderungen bei flush selbst sieht.

Zweitens: Wenn in reserve(qty) eine Exception auftritt (z.B. „nicht genug Ware“), wird die gesamte Operation vollständig zurückgerollt. Die Datenbank wird keinen „halberfolgreichen Bestellbestätigung“ sehen. Wir haben wieder das „alles oder nichts“.

Drittens hören wir auf, versehentlich einen detached-flow zu provozieren. Das heißt, wir verwenden merge seltener als „magischen Hammer“ und leben öfter im verwalteten managed-Update-Stil, der im SQL einfacher vorherzusagen ist.

Im SQL-Log sieht das normalerweise einfacher und sauberer aus:

-- Eine Transaktion für den gesamten confirmOrder(...)
SELECT ... FROM purchase_order WHERE id = ?;
SELECT ... FROM inventory_item WHERE product_id = ?;

-- Bei flush/commit synchronisiert Hibernate die Änderungen in einem Batch innerhalb derselben Tx
UPDATE purchase_order SET status = ? WHERE id = ?;
UPDATE inventory_item SET available_qty = ?, reserved_qty = ? WHERE id = ?;

Ja, die Reihenfolge und die Details hängen vom Mapping und davon ab, was genau geändert wird, aber die Kernaussage bleibt: Sie sehen ein einheitliches Szenario, keine Ansammlung von separaten „in die DB gegangen – zurückgekommen – wieder gegangen“.

Um den Nagel endgültig in den Sarg von „Transaktionen im Repository“ zu schlagen, vergleichen wir dies in einer Tabelle:

Eigenschaft „Transaktionen in Repositories“ „Transaktion im Service“
Anzahl der Unit of Work pro Geschäftsoperation Oft mehrere Genau einer
Entitätszustand zwischen Aufrufen Oft detached managed innerhalb der Methode
Rolle von save Oft erzwungenes merge Normalerweise nur für neue Entitäten nötig
Risiko von „teilweise gespeicherten“ Daten Hoch Niedrig (Atomarität)
Vorhersagbarkeit des SQL-Pfads Mittel/Niedrig Hoch
Diagnose anhand von Logs „Warum ging es so oft hin?“ „Hier ist das Szenario, hier ist sein SQL“

Und das alles – ohne fortgeschrittene Techniken. Einfach der richtige Ort für die Grenze.

Transaktionsgröße: eng und weit

Nachdem Sie die Idee „die Transaktion lebt im Service“ akzeptiert haben, tritt der zweite typische Fehler auf: die Schaukel. Zuerst lebt man mit Transaktionen in den Repositories (zu eng), dann bekommt man Angst und macht eine riesige @Transactional „für alle Fälle“ (zu weit). Und bekommt wieder Probleme – nur anderer Art.

Eine zu enge Transaktion zerfällt, wie wir gesehen haben, in detached-flow, merge, wiederholte SELECT-Anweisungen und „halberfolgreiche“ Operationen. Eine zu weite Transaktion zerbricht das System anders: Sie halten die Datenbank länger „unter Spannung“ als nötig. Das äußert sich in langen Sperren, seltsamen Verzögerungen, wachsender Last und manchmal auch einfach „alles ist langsam, aber unklar warum“. In einem Übungsprojekt mag das nicht so dramatisch sein, aber im echten Leben ist es, als würde man eine Kaffeemaschine in den „Endlos-Zubereitungsmodus“ versetzen: Es gibt zwar Kaffee, aber die ganze Küche ist lahmgelegt.

Das praktische Kriterium für die Transaktionsgröße klingt langweilig, funktioniert aber: Die Transaktion sollte den minimalen Code abdecken, der benötigt wird, um ein einziges konsistentes Ergebnis zu erhalten. In der Regel leben darin das Lesen von Daten für Entscheidungen, das Prüfen von Invarianten und das Ändern des Zustands. Alles, was nicht Teil des „konsistenten Ergebnisses in der Datenbank“ ist, sollte man besser nicht hineinziehen.

Zum Beispiel: Wenn Sie nach der Bestellbestätigung eine hübsche PDF erstellen, eine E-Mail senden, einen externen HTTP-Dienst aufrufen oder eine lange Rabattberechnung durchführen wollen – das ist fast nie etwas, das man innerhalb derselben DB-Transaktion halten möchte. In unserem Kurs halten wir das Projekt bewusst einfach und gehen nicht auf Integrationen ein, aber die Regel ist trotzdem nützlich zu merken: Eine kurze Transaktion ist einfacher zu diagnostizieren und kostet das System weniger, während eine integrale Transaktion die Datenkorrektheit bewahrt.

5. Techniken für Service-Code

Nachdem der Ort der Transaktion richtig gewählt ist, möchte man den zweiten Schritt machen: Den Service-Code so schreiben, dass er bequem zu lesen und mit dem SQL-Log abzugleichen ist. Im Laborprojekt ist das besonders wichtig: Wir lernen nicht „damit es funktioniert“, sondern „damit verständlich ist, warum es genau so funktioniert“.

Die erste Technik: Änderungen durch Domänenmethoden ausdrücken, nicht durch eine Ansammlung von Settern im Service. Ja, man kann order.setStatus(CONFIRMED) schreiben, aber es ist viel verständlicher, order.confirm() zu lesen. Erstens macht es den Szenario-Code einem Szenario ähnlich, nicht einer Feldmanipulation. Zweitens ist eine Domänenmethode ein guter Ort für kleine Regeln: „Eine stornierte Bestellung kann nicht bestätigt werden“, „Eine Bestellung kann nicht zweimal bestätigt werden“ und so weiter. Und diese Regeln leben dann neben den Daten, nicht verstreut über Services.

Die zweite Technik: Invarianten dort prüfen, wo sie logisch hingehören. Zum Beispiel ist die Bestandsreservierung nicht einfach „minus und plus“, sondern eine Operation mit Einschränkungen. In InventoryItem könnte das so aussehen:

public void reserve(int qty) {
    // Invariante: Es kann nicht mehr reserviert werden als verfügbar
    if (availableQty < qty) {
        // Wichtig: Die Exception sollte das Szenario „abbrechen“
        // Bei einer Service-@Transactional führt dies zum Rollback der gesamten Operation
        throw new NotEnoughStockException(product.getId(), availableQty, qty);
    }

    // Die eigentliche Geschäftsoperation: Menge von available nach reserved verschieben
    availableQty -= qty;
    reservedQty += qty;
}

Beachten Sie, dieser Code ist sehr „Java-einfach“, ohne jede Magie. Aber in Kombination mit @Transactional im Service hat er einen wichtigen Effekt: Wenn die Exception geworfen wird, wird die Transaktion nicht committet, und Sie erhalten kein teilweise angewandtes Szenario.

Die Exception selbst kann man maximal einfach halten, solange sie sinnvoll ist und (in unserem Kontext) unchecked ist, um den Code nicht in „try/catch der bloßen Form halber“ zu verwandeln:

public class NotEnoughStockException extends RuntimeException {
    public NotEnoughStockException(Long productId, int have, int need) {
        // Die Nachricht sollte helfen zu verstehen, „wie viel war“ und „wie viel gewünscht wurde“
        super("Product " + productId + ": have=" + have + ", need=" + need);
    }
}

Die dritte Technik: Den externen Code nicht die Repositories „orchestrieren“ lassen. Ein sehr häufiger architektonischer Schmerz sieht so aus: Der Controller (oder eine andere externe Schicht) ruft orderRepository auf, dann inventoryRepository, dann noch etwas, und dann versucht man „irgendwo“ zu verstehen, warum der Zustand seltsam ist. Richtiger ist, wenn die externe Schicht dem Service sagt: „Mach den Use Case“, und der Service führt dann intern die nötigen Schritte aus. Es geht nicht um „Schönheit“ – es geht darum, dass die Verantwortungsgrenze mit der Transaktionsgrenze zusammenfällt.

Die vierte Technik: Keine managed-Entitäten nach außen zurückgeben, „damit das Fest weitergeht“. Selbst wenn Sie eine Transaktion innerhalb des Services haben, endet sie beim Verlassen der Methode. Wenn Sie eine Entity nach außen zurückgeben und jemand beginnt, ihre lazy-Felder anzufassen oder ihren Zustand zu ändern, landen Sie wieder in der Welt der detached und unerwarteten Konsequenzen. Es ist viel beruhigender, wenn der Service eine ID, einen Status, ein DTO oder eine andere „externe“ Darstellung des Ergebnisses zurückgibt, nicht das von Hibernate verwaltete Objekt selbst.

6. Typische Fehler bei der Arbeit mit Transaktionen

Bevor wir im Tag weitergehen, ist es nützlich, ein paar „Fallstricke“ festzuhalten, auf die fast alle treten, die zum ersten Mal bewusst Transaktionen setzen. Diese Fehler sind besonders tückisch, weil der Code wochenlang funktionieren kann, und dann eines Tages in der Produktion explodiert (oder im Übungsprojekt genau in dem Moment, in dem Sie das SQL-Log einschalten und denken „jetzt verstehe ich alles“).

Fehler Nr. 1: Sich auf die Transaktionalität des Repositories als Transaktionalität der Geschäftsoperation verlassen.
Ja, die Methoden von Spring Data JPA sind oft standardmäßig transaktional, aber das sind Transaktionen pro Aufruf. Sobald Ihre Operation zwei Repositories oder zwei Schritte (lesen → prüfen → ändern) umfasst, verwandeln Sie ohne Service-Grenze ein Szenario in eine Menge unabhängiger Mini-Szenarien. Das führt fast garantiert zu detached-flow, merge und riskanten „halberfolgreichen“ Zuständen.

Fehler Nr. 2: Mechanisch nach jedem Setter save aufrufen.
Wenn die Service-Methode transaktional ist und die Entität innerhalb von ihr geladen wurde, ist sie managed. Hibernate sieht die Änderungen bei flush ohnehin. Mechanisches save verbessert normalerweise nichts oder verschlechtert es (z.B. provoziert es versehentlich merge-Semantik, unnötige Abfragen oder einen vorzeitigen flush zum falschen Zeitpunkt).

Fehler Nr. 3: Eine Operation auf mehrere Service-Methoden ohne gemeinsame Transaktion aufteilen.
Manchmal macht ein Entwickler eine „schöne Dekomposition“: Eine Methode ändert die Bestellung, eine andere die Bestände, eine dritte noch etwas, und alles wird nacheinander aufgerufen. Wenn es keinen gemeinsamen Service-Einstiegspunkt mit einer Transaktion gibt, haben Sie wieder fragmentierte Unit of Work. Der Code sieht „architektonisch“ aus, aber das Verhalten der Persistenzschicht wird zufällig.

Fehler Nr. 4: Die Transaktion „für alle Fälle“ zu weit machen.
Wenn @Transactional zu einer Hülle „um die Hälfte der Anwendung“ wird, halten Sie die Datenbank und den Persistenzkontext länger als nötig. Im Übungsprojekt äußert sich das als verrauschtes SQL und schwierige Diagnose, im echten System oft als Verzögerungen und Konkurrenzprobleme. Die Transaktion sollte genau so groß sein, dass sie ein konsistentes Ergebnis garantiert, und nicht größer.

Fehler Nr. 5: Entitäten nach außen zurückgeben und erwarten, dass „sie noch lebendig sind“.
Die Rückgabe einer Entität aus dem Service nach außen sieht praktisch aus, bis man auf lazy loading und detached-Zustand stößt. Beim Verlassen des Services endet die Transaktion, der Persistenzkontext wird geschlossen, und jeder Versuch, die Entität „nachzulesen“ oder „leicht zu korrigieren“, findet außerhalb der verwalteten Welt statt, in der Hibernate Ihr Verbündeter war. Das Ergebnis: Entweder Sie fangen Exceptions oder Sie rutschen unbemerkt zurück in einen merge-orientierten Stil.

1
Aufgabe
Hibernate deep-dive, Level 17, Lektion 1
Gesperrt
Bestellbestaetigung und Lagerreservierung in einer Servicemethode
Bestellbestaetigung und Lagerreservierung in einer Servicemethode
1
Aufgabe
Hibernate deep-dive, Level 17, Lektion 1
Gesperrt
Aktivierung von Kunde und Profil als eine atomare Operation
Aktivierung von Kunde und Profil als eine atomare Operation
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION