CodeGym /Kurse /Spring Data JPA /Regel zur Werkzeugauswahl und OSIV

Regel zur Werkzeugauswahl und OSIV

Spring Data JPA
Level 22, Lektion 4
Verfügbar

1. Auswahlregel: Ergebnisform → Werkzeug

Wenn Sie sich aus dieser Vorlesung nur einen Satz merken, dann sei es dieser: „Zuerst entscheiden wir, welches Ergebnis der Use-Case benötigt, dann wählen wir die Lesemethode aus.“ Das Problem der meisten N+1- und Lazy-Fehler liegt nicht darin, dass der Entwickler die Annotationen nicht kennt, sondern darin, dass er die Antwortform nicht formuliert und dem Code erlaubt hat, Daten überall „nachzuladen“.

Stellen Sie sich vor, Sie gehen in einen Laden (unseren mini-shop) und sagen: „Geben Sie mir … ähm … irgendetwas“. Der Verkäufer könnte Ihnen sowohl Batterien als auch einen Kühlschrank und eine Sammlung von Java Concurrency in Practice bringen. Genauso verhält es sich mit ORM: Wenn Sie die Ergebnisform nicht festlegen, wird es durch Lazy Loadings „raten“, und jeder Ihrer Getter-Aufrufe wird zu einem potenziellen SQL-Query.

In unserem Projekt shop-data-jpa ist es praktisch, sich mindestens drei grundlegende Formen von Read-Use-Cases zu merken:

1) Karte (ein Objekt, aber „detailliert“). Beispiel: Bestellkarte nach orderNumber, mit benötigten Positionen und Artikeln.
2) Liste (viele Objekte, aber „kurz“). Beispiel: Liste der Bestellungen im Status PAID oder Liste der Artikel im Katalog mit Sortierung und Seiten.
3) Zusammenfassung / Summary (noch kürzer, möglicherweise Aggregate). Beispiel: Bestellnummer + Betrag + Anzahl der Positionen.

Und hier beginnt die Magie … aber eine normale, technische: Dasselbe Domänengebiet erfordert verschiedene Lesemodelle. Für die Karte kann ein Entity-basiertes Lesen (mit Nachladen der Beziehungen) gerechtfertigt sein, während eine Liste öfter eine Projection benötigt, sonst erhalten Sie entweder N+1 oder lesen einfach zehnmal mehr Daten als nötig.

Um die Repositories nicht in eine „Universalkombine“ zu verwandeln, ist es nützlich, Methoden so zu benennen, dass ihre Absicht aus der Signatur ersichtlich ist. Vergleichen Sie zwei Methoden: Sie sehen ähnlich aus, beschreiben aber tatsächlich verschiedene Read-Use-Cases.

import java.util.Optional;

public interface CustomerOrderRepository {

    // "Karte": geben ein Entity zurück, das (höchstwahrscheinlich) vorab mit den benötigten Beziehungen vorbereitet werden muss.
    Optional<CustomerOrder> findCardByOrderNumber(String orderNumber);

    // "Zusammenfassung": geben eine Projection/Read-Modell zurück, hier gibt es keine Lazy-Navigation über Entitäten.
    Optional<OrderSummaryRow> findSummaryByOrderNumber(String orderNumber);
}

Die erste Methode verspricht eine „Karte“ (höchstwahrscheinlich mit den benötigten Beziehungen), die zweite eine „Zusammenfassung“ (Projection). Und ein wichtiger psychologischer Effekt: Wenn Sie OrderSummaryRow sehen, greift die Hand nicht mehr nach order.getItems() – weil order hier gar nicht existiert.

2. Auswahlmatrix: Fetch vs. Graph vs. Projection

Wenn es mehrere Werkzeuge gibt, macht das Gehirn eines Anfängers oft einen gefährlichen Trick: Es wählt eines aus, das es mag, und beginnt damit alles zu bearbeiten, wie mit einem Hammer. Das führt zu der Philosophie: „Wenn du join fetch hast, ist alles um dich herum ein Nagel“. Im Leben endet das ungefähr so wie mit einem echten Hammer: Sie versuchen eines Tages damit die Batterie in der Fernbedienung zu wechseln.

Lassen Sie uns eine kompakte „Entscheidungsmatrix“ erstellen, die hilft, das Werkzeug entsprechend dem Use-Case-Sinn auszuwählen. Das ist kein Naturgesetz und keine „einzig richtige Architektur“, aber für Juniorebene ist es eine sehr praktische Stütze.

Read-Use-Case (in menschlichen Worten) Was wir zurückgeben Wählen wir normalerweise Warum das logisch ist
„Produktkarte benötigt, und Kategorie wird definitiv gebraucht“ Product join fetch oder @EntityGraph Die Entität wird vollständig benötigt, aber eine Beziehung muss sofort vorbereitet werden
„Bestellkarte mit Positionen und Artikeln benötigt“ CustomerOrder join fetch oder @EntityGraph Die Karte ist ein „tiefes“ Lesen, benötigt Ladekontrolle
„Liste der Bestellungen benötigt: Nummer, Status, Betrag“ projection (record / interface) projection-basiertes Lesen Die Liste ist „Breite“, eine Entität ist fast immer überflüssig
„Zusammenfassung benötigt: Nummer + Betrag + itemsCount“ projection + Aggregate projection (select new …) Man kann die Kollektionen gar nicht anfassen, sondern das Ergebnis per Query sammeln
„Nach dem Lesen werden wir das Objekt innerhalb der Transaktion ändern“ entity Entity-basiertes Lesen + benötigter Fetch-Plan Projections sind nicht managed, sie zu ändern ist sinnlos

Jetzt kurz zum „internen Unterschied“ zwischen join fetch und @EntityGraph, weil beide eine ähnliche Aufgabe lösen: Beziehungen innerhalb eines Read-Use-Cases nachladen.

join fetch ist gut, wenn Sie sowieso @Query benötigen, weil die Abfragelogik selbst nicht durch eine einfache derived-Methode ausgedrückt wird. Sie schreiben JPQL und legen direkt darin fest, was genau nachgeladen wird.

import org.springframework.data.jpa.repository.Query;
import java.util.Optional;

public interface ProductRepository {

    @Query("""
           select p
           from Product p
           join fetch p.category
           where p.id = :id
           """)
    // join fetch: sagen Hibernate explizit, category im selben Query zu laden,
    // damit später kein Lazy-SQL auf p.getCategory() erfolgt.
    Optional<Product> findCardById(long id);
}

@EntityGraph ist praktischer, wenn die Abfrage selbst „so einfach“ ist (z.B. findById, findByOrderNumber, findByStatus) und Sie den Ladeplan separat angeben möchten. Das heißt, Sie ändern nicht die Auswahllogik, Sie ändern nur den Ladeplan.

import org.springframework.data.jpa.repository.EntityGraph;
import java.util.Optional;

public interface ProductRepository {

    // EntityGraph: die Auswahllogik bleibt derived (find...),
    // und der Ladeplan wird separat angegeben (was genau nachgeladen werden soll).
    @EntityGraph(attributePaths = "category")
    Optional<Product> findCardById(long id);
}

Projection hingegen ist eine ganz andere Denkweise: Anstatt zu diskutieren „welche Beziehungen nachladen“, sagen Sie „ich brauche keine Entität“. Das ist oft die ehrlichste und günstigste Option für Liste/Summary.

import java.math.BigDecimal;

// Projection/Read-Modell für Zusammenfassung: nur das, was der Use-Case wirklich benötigt.
public record OrderSummaryRow(String orderNumber, BigDecimal totalAmount) {}

3. Eine Bestellung nach Nummer – verschiedene Leseformen

Das Schönste (und das Heimtückischste) an Spring Data JPA: Sie können die Aufgabe fast immer auf mehrere Arten lösen. Ein Anfänger freut sich zuerst, leidet dann: „Also welcher ist der richtige?“ Spoiler: Der richtige ist der, der der Use-Case-Form entspricht und die Anwendung nicht dazu zwingt, Überflüssiges zu lesen oder „nachzuladen“.

Nehmen wir einen konkreten Einstiegspunkt aus unserem mini-shop: Bestellung nach Nummer. Aber es ist wichtig, sich nicht selbst zu betrügen: „Bestellkarte“ und „Bestellzusammenfassung“ sind bereits unterschiedliche Read-Use-Cases. Daher ist es richtiger, nicht drei Implementierungen desselben Szenarios zu vergleichen, sondern mehrere ehrliche Leseformen um dieselbe Bestellung herum.

Variante A: join fetch und Plan in JPQL

Dieser Ansatz ist gut, wenn Sie mit einer Stelle (Abfragetext) sowohl die Filterung als auch das Laden der Beziehungen festlegen möchten. Üblicherweise macht man das für Karten: eine Bestellung → viele Details.

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface CustomerOrderRepository {

    @Query("""
           select distinct o
           from CustomerOrder o
           left join fetch o.items i
           left join fetch i.product
           where o.orderNumber = :orderNumber
           """)
    // distinct wird benötigt, weil das Fetch einer Kollektion die Zeilen im SQL-Ergebnis vervielfacht
    // (eine Zeile pro Bestellposition), wir logisch aber nur eine CustomerOrder benötigen.
    Optional<CustomerOrder> findCardByOrderNumber(@Param("orderNumber") String orderNumber);
}

Beachten Sie distinct. Es ist hier nicht zur Verschönerung und nicht „weil es so üblich ist“. Wenn Sie eine Kollektion fetchen, enthält das SQL-Ergebnis doppelte Zeilen für die Bestellung (eine pro Position). distinct auf JPQL-Ebene hilft Hibernate, die „logisch eindeutigen“ CustomerOrder im Ergebnis zu sammeln.

Variante B: @EntityGraph und Ladeplan

Wenn es Ihnen gefällt, dass die Repository-Methode wie ein normaler derived query aussieht, Sie aber Beziehungen für den Use-Case nachladen möchten, ist @EntityGraph sehr angenehm. Es macht die Absicht „lade das hier nach“ sichtbar, ohne die Abfrage umzuschreiben.

import org.springframework.data.jpa.repository.EntityGraph;

import java.util.Optional;

public interface CustomerOrderRepository {

    // Wir sagen: für die Karte benötigen wir items und product innerhalb items.
    // Das Kriterium ist immer noch derived nach orderNumber, und der Methodenname spiegelt ehrlich den Karten-Use-Case wider.
    @EntityGraph(attributePaths = {"items", "items.product"})
    Optional<CustomerOrder> findCardByOrderNumber(String orderNumber);
}

Diese Variante liest sich oft leichter als langes JPQL. Ihr Nachteil ist ebenfalls technisch: Wenn Sie die Filterung selbst verkomplizieren müssen, werden Sie trotzdem zu @Query wechseln. Das heißt, @EntityGraph ist nicht „immer besser“, sondern „besser, wenn die Abfrage ohnehin einfach ist“.

Variante C: Projection für Zusammenfassung

Und jetzt eine wichtige Wendung: Manchmal scheint eine Bestellkarte nötig, aber eigentlich braucht die äußere Schicht nur kurze Informationen: Nummer, Betrag, Anzahl der Positionen. In diesem Fall kann es unnötig sein, die items-Kollektion als Entitäten zu laden.

Dann kann man statt einer Entity ein Read-Modell zurückgeben. Zum Beispiel so:

import java.math.BigDecimal;

// Read-Modell einer kurzen Zusammenfassung der Bestellung, kein Entity und keine Karte mit Beziehungen.
public record OrderSummaryRow(String orderNumber, BigDecimal totalAmount, long itemsCount) {}

Und die Abfrage:

import org.springframework.data.jpa.repository.Query;

import java.util.Optional;

public interface CustomerOrderRepository {

    @Query("""
           select new com.example.shopdatajpa.ordering.query.OrderSummaryRow(
               o.orderNumber, o.totalAmount, count(i)
           )
           from CustomerOrder o
           left join o.items i
           where o.orderNumber = :orderNumber
           group by o.orderNumber, o.totalAmount
           """)
    // Wichtig: Wir fetchen items nicht als Entitäten.
    // Wir bitten die DB, sofort count(i) zu berechnen und die fertige Zusammenfassung zurückzugeben.
    Optional<OrderSummaryRow> findSummaryByOrderNumber(String orderNumber);
}

Hier ziehen wir items gar nicht als Kollektion von Entitäten heraus. Wir bitten die DB, sofort count(i) zu berechnen und die fertige Zusammenfassung zurückzugeben. Das ist ein anderer Ansatz, ein anderes Ergebnis und andere Lesekosten.

Und hier entsteht die Auswahlregel: Wenn der Use-Case wirklich eine Zusammenfassung ist, benötigen Sie join fetch nicht, nicht weil es schlecht ist, sondern weil es eine andere Aufgabe löst.

In allen drei Zweigen gibt es eine gemeinsame Disziplin: Wir wählen die Ergebnisform und den Leseplan innerhalb des Service-Use-Cases, solange SQL noch unter Kontrolle ist. OSIV erscheint genau deshalb praktisch, weil es erlaubt, diese Entscheidung aufzuschieben: Entity nach außen zurückzugeben und zu hoffen, dass die benötigten Beziehungen irgendwie von selbst nachgeladen werden. Jetzt sehen wir, warum sich diese „Praktikabilität“ schnell in ein Problem verwandelt.

4. OSIV: Was ist das und wo liegt der Haken

OSIV (Open Session in View) – eine dieser Sachen, die genau bis zu dem Moment praktisch erscheinen, in dem Sie anfangen zu verstehen, wo genau SQL ausgeführt wird. Historisch half OSIV, Beziehungen „lazy“ bereits auf der Ebene der Web-Ansicht (view) nachzuladen, um LazyInitializationException zu vermeiden. Aber in modernen Backend-Projekten wird dies oft zu einer „Erlaubnis für Überraschungen“.

In Spring Boot wird das normalerweise durch die Einstellung spring.jpa.open-in-view ausgedrückt. Wenn aktiviert (oder standardmäßig in einer Web-Anwendung aktiviert), hält Boot den EntityManager (und damit den persistence context) während der gesamten HTTP-Anfrage offen. Und dann können Lazy-Beziehungen sogar nachgeladen werden, nachdem Ihre Servicemethode bereits abgeschlossen ist.

Um nicht abstrakt zu bleiben, schauen wir uns einen „schlechten, aber realistischen“ Code an. Angenommen, wir haben einen Service, der einfach eine Bestellung findet und ein Entity zurückgibt, ohne sich um das Laden der items zu kümmern.

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

@Service
public class OrderService {
    private final CustomerOrderRepository orderRepository;

    public OrderService(CustomerOrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional(readOnly = true)
    public CustomerOrder findByNumber(String orderNumber) {
        // Normaler derived-Methode: Entity wird ohne vorbereiteten Karten-Fetch-Plan zurückgegeben.
        return orderRepository.findByOrderNumber(orderNumber).orElseThrow();
    }
}

Und irgendwo „außen“ (im Controller, in der View, in einem beliebigen Adapter) machen wir Folgendes:

CustomerOrder order = orderService.findByNumber(orderNumber);
// Bei eingeschaltetem OSIV kann dieser Aufruf bereits außerhalb des Service/Transaktion SQL auslösen.
int itemsCount = order.getItems().size(); // hier kann SQL abfliegen

Mit aktiviertem OSIV funktioniert es „einfach“. Mit deaktiviertem OSIV – fällt LazyInitializationException. Und hier ist wichtig: Der Fehler ist keine „Katastrophe“, sondern ein Signal, dass Sie Daten jenseits der Use-Case-Grenze nachladen.

Zur visuellen Veranschaulichung folgt ein Diagramm dessen, was bei aktiviertem OSIV passiert:

sequenceDiagram
    participant C as "Controller / äußere Schicht"
    participant S as "Service (@Transactional readOnly)"
    participant R as Repository
    participant DB as PostgreSQL

    C->>S: findByNumber(orderNumber)
    S->>R: findByOrderNumber(...)
    R->>DB: select ... from customer_order ...
    DB-->>R: order row
    R-->>S: CustomerOrder (items LAZY)
    S-->>C: CustomerOrder
    C->>DB: "select ... from order_item ... (lazy load durch OSIV)"

Schlüsselmoment: SQL wird plötzlich im Controller/äußeren Schicht ausgeführt. Und das ist mindestens unerwartet, maximal – der Weg zu N+1, den Sie in den Servicelogs nicht einmal bemerkt haben.

5. OSIV – schlechter Default in Data-Layer-First-Projekten

OSIV wird oft mit dem Satz verteidigt: „naja, dafür fällt LazyInitializationException nicht“. Das ist ungefähr wie zu sagen: „Ich schnalle mich nicht an, dafür drückt der Gurt nicht“. Formal ist das Problem „drückt nicht“ gelöst, aber der Preis ist seltsam. In einem Data-Layer-First-Projekt (wie unserem shop-data-jpa) bricht OSIV das methodische Hauptprinzip: der Use-Case sollte im Service abgeschlossen sein, nicht „irgendwo nachgeladen werden“.

Das erste Problem von OSIV – es verwischt die Verantwortungsgrenze. Wenn die äußere Schicht order.getItems() lazy nachladen kann, dann hört der Service auf, der Ort zu sein, an dem die Entscheidung getroffen wird, „was dieses Szenario benötigt“. Das heißt, der Ladeplan hört auf, ein Plan zu sein, und wird zu „wer zuerst den Getter aufruft, zahlt für SQL“. Das ist schlecht für die Lesbarkeit des Codes und für das Verständnis der Last.

Das zweite Problem von OSIV – es maskiert N+1. Sie können eine „harmlose“ Schleife im Controller schreiben, die für jede Bestellung getItems() aufruft, und bei aktiviertem OSIV werden Sie keine Exception sehen. Sie werden funktionierenden Code sehen … der z.B. 1 Query auf die Bestellliste + 20 Queries auf die Positionen + noch 50 Queries auf die Artikel macht. Das heißt, N+1 verwandelt sich nicht in einen offensichtlichen Fehler, sondern in ein leises Performance-Leck.

Das dritte Problem von OSIV – es macht SQL zu einem „zufälligen Nebeneffekt“ von gewöhnlichem Java-Code. In einem guten Projekt befindet sich SQL entweder im Repository oder (zumindest) innerhalb der Service-@Transactional(readOnly = true)-Methode. Mit OSIV wird jeder .getXxx() potenziell zu einem Datenbank-Query. Das ist selbst erfahrenen Entwicklern unangenehm, und Anfängern besonders: Sie beginnen, Entitäten wie „explosive Objekte“ zu fürchten.

Und noch ein subtiler Punkt, der OSIV mit dem Thema der letzten Tage verbindet: Wir kennen bereits den persistence context und dirty checking. Wenn der persistence context länger lebt als nötig, steigt die Chance, versehentlich eine Änderung an einer Entität an einer unerwarteten Stelle vorzunehmen und im ungünstigsten Moment „warum wurde es aktualisiert?“ zu bekommen. Auch wenn Sie in dieser Vorlesung keine Write-Operationen durchführen, rächt sich die architektonische Gewohnheit „lass es länger leben“ später meistens.

Fazit: OSIV kann vor der Exception retten, aber auf Kosten einer architektonischen „Verschleierung“. In einem Lernprojekt ist das besonders schädlich, weil Sie gerade lernen, die Kausalzusammenhänge zwischen Code, Transaktion und SQL zu erkennen.

6. Leben ohne OSIV: Der Service bereitet Daten vor

OSIV zu deaktivieren ist, als würde man in einem Raum gutes Licht einschalten, in dem man zuvor mit einer Taschenlampe herumlief. Zuerst tut es weh (Probleme kommen zum Vorschein), aber dann stellt sich plötzlich heraus, dass der Raum Wände, Türen und allgemein eine verständliche Geometrie hat. Wichtig ist, nicht zu versuchen, „die Dunkelheit zurückzubringen“, sondern zu lernen, Lesevorgänge normal zu entwerfen: über Fetch-Plan und Ergebnisform.

OSIV deaktivieren

In unserem Kurs ist die Web-Schicht nicht der Mittelpunkt, aber Thin-Adapter werden manchmal für Smoke-Tests hinzugefügt. Wenn Sie spring-boot-starter-web eingebunden haben, ist die Konfiguration normalerweise so:

spring:
  jpa:
    open-in-view: false # Verbietet Lazy-Nachladen außerhalb des Service-Use-Cases

Der Sinn ist nicht, OSIV nur aus Prinzip zu „besiegen“. Der Sinn ist, dass jedes Lazy-Nachladen außerhalb des Service-Use-Cases sofort als Designfehler des Lesevorgangs erkennbar wird.

Der Service gibt fertige Daten zurück

Dann greift unsere Auswahlregel. Wenn die äußere Schicht (Controller, Job, CLI) etwas ausgeben soll, muss sie bereits eine fertige Datenform erhalten. Entweder ein Entity, bei dem alles Nötige innerhalb der Transaktion geladen ist, oder eine Projection/DTO, die gar keine Lazy-Navigation erfordert.

Hier ein Beispiel, wo die äußere Schicht eine kurze Zusammenfassung der Bestellung benötigt: Der Service wählt selbst die fertige Leseform, und es entweicht kein Entity mit Lazy-Überraschungen nach außen.

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

@Service
public class OrderReadService {
    private final CustomerOrderRepository orderRepository;

    public OrderReadService(CustomerOrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional(readOnly = true)
    public OrderSummaryRow loadOrderSummary(String orderNumber) {
        // Die Entscheidung über die Ergebnisform wird hier, innerhalb des Service-Use-Cases, getroffen.
        return orderRepository.findSummaryByOrderNumber(orderNumber).orElseThrow();
    }
}

Wenn die äußere Schicht genau eine Bestellkarte benötigt, wählt der Service an derselben Stelle findCardByOrderNumber(...). Das Prinzip ändert sich nicht: Nach außen geht eine bereits fertige Datenform, kein Entity mit dem Versprechen „laden wir später nach“.

Warum das gut funktioniert:

- orderRepository.findSummaryByOrderNumber(...) gibt ein fertiges Read-Modell zurück, kein Entity mit Lazy-Beziehungen.
- Die Entscheidung über die Leseform wird innerhalb der Servicemethode getroffen, daher breitet sich SQL nicht auf äußere Schichten aus.
- Die äußere Schicht erhält OrderSummaryRow, wo ORM nichts mehr „auf gut Glück nachladen“ muss.

Die äußere Schicht wird automatisch „dünn“

Wenn der Service ein fertiges Read-Modell zurückgibt, sieht die äußere Schicht so aus, wie sie in einem Data-Layer-First-Projekt aussehen sollte: ohne Fetch-Logik und ohne unnötigen Zugriff auf den Entity-Graphen.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
    private final OrderReadService orderReadService;

    public OrderController(OrderReadService orderReadService) {
        this.orderReadService = orderReadService;
    }

    @GetMapping("/orders/{orderNumber}/summary")
    public OrderSummaryRow getOrderSummary(@PathVariable String orderNumber) {
        // Der Controller greift nicht auf den Entity-Graphen zu und löst kein SQL aus.
        return orderReadService.loadOrderSummary(orderNumber);
    }
}

Ja, das ist bereits „Web“ – aber beachten Sie, dass wir nicht in REST-Design abgleiten, keine DTO-Mapper diskutieren und keine vollständige API bauen. Wir zeigen nur das Prinzip: Der Controller sollte nicht der Ort sein, an dem plötzlich SQL ausgeführt wird.

Schnelle Auswahl des Werkzeugs im Kopf

Hier ein kleiner mentaler „Flowchart“, den Sie bequem durchgehen können, wenn Sie einen neuen Read-Use-Case schreiben:

flowchart TD
    A[Neuer Read-Use-Case] --> B{Wird Entität als managed Objekt für Änderungen benötigt?}
    B -->|Ja| C[Entity-basiertes Lesen]
    C --> D{Beziehungen in diesem Use-Case benötigt?}
    D -->|Ja| E[join fetch oder @EntityGraph]
    D -->|Nein| F[Normaler find...]
    B -->|Nein| G{Liste/Zusammenfassung benötigt?}
    G -->|Ja| H[Projection-basiertes Lesen]
    G -->|Nein| I[Optional: Entity oder Projection nach Antwortform]

Der Sinn des Schemas ist nicht, Sie „in das richtige Muster zu zwingen“. Der Sinn ist, dass Sie sich jedes Mal Fragen zur Ergebnisform und dazu stellen, wo der Use-Case endet. Wenn diese Fragen ehrlich gestellt werden, werden die Werkzeuge fast automatisch ausgewählt.

7. Typische Fehler bei OSIV und Lesevorgängen

Fehler Nr. 1: OSIV als „offizielle Methode zum Umgang mit Lazy“ zu betrachten.
Das sieht verlockend aus: Einstellung aktiviert – und LazyInitializationException verschwindet. Aber mit der Exception verschwindet auch das Verständnis, wo genau SQL ausgeführt wird. Nach ein paar Tagen fangen Sie N+1 nicht im Service, sondern im Controller, und beginnen zu vermuten, dass Hibernate „sein eigenes Leben führt“, obwohl tatsächlich das Leseszenario schuld ist.

Fehler Nr. 2: Entity nach außen zurückzugeben, „weil es einfacher ist“.
Auf kurze Distanz ist es tatsächlich einfacher, CustomerOrder zurückzugeben und „irgendwann später“ items zu holen. Auf mittlere Distanz erhalten Sie ein Gemisch aus Domänenmodell, Fetch-Logik und zufälligen SQL-Queries an Stellen, wo niemand sie erwartet. Wenn die äußere Schicht Daten benötigt – soll sie eine Projection/DTO oder ein bereits vom Service vorbereitetes Entity erhalten.

Fehler Nr. 3: Leseprobleme durch Verschieben von @Transactional in den Controller zu beheben.
Nach dem Deaktivieren von OSIV zieht ein Anfänger manchmal eine „logische“ Schlussfolgerung: Da Lazy außerhalb der Transaktion fehlschlägt, muss man die Transaktion näher an die Fehlerstelle öffnen, also im Controller. Das ist fast immer ein schlechter Default, weil Sie die Web-Anfrage in eine unvorhersehbare Unit of Work verwandeln und die Use-Case-Grenze erneut verwischen. Die Transaktion sollte im Service sein, und der Controller sollte ein dünner Adapter sein.

Fehler Nr. 4: Verwendung von collection join fetch in paged-Listen „weil ein Query ja besser ist“.
Es ist sehr leicht, findAllWithItems(Pageable pageable) zu schreiben und sich zu freuen, dass N+1 verschwunden ist. Aber dann stoßen Sie auf Duplikate, seltsame Paginierung, aufgeblähte Ergebnisse und „warum ist die Seite mal leer, mal mit Wiederholungen“. Für Listen ist normalerweise eine Projection (oder ein sehr enger Fetch-Plan) besser, und Collection-Fetch sollte man für Karten reservieren.

Fehler Nr. 5: Projection wie eine Entität „aktualisieren“ zu wollen.
Eine Projection ist ein Read-Modell. Sie befindet sich nicht im managed-Zustand und nimmt nicht am dirty checking teil. Wenn Sie Daten ändern müssen – laden Sie die Entität in der Transaktion, ändern Sie sie als managed Entity und fixieren Sie die Änderungen über die normale JPA-Mechanik. Eine Projection in einem Write-Use-Case bedeutet fast immer, dass Sie die Lese- und Änderungsform verwechselt haben.

1
Aufgabe
Spring Data JPA, Level 22, Lektion 4
Gesperrt
Bestellkarte mit deaktiviertem OSIV
Bestellkarte mit deaktiviertem OSIV
1
Aufgabe
Spring Data JPA, Level 22, Lektion 4
Gesperrt
Auswahl des Werkzeugs nach Ergebnisform für `Product`
Auswahl des Werkzeugs nach Ergebnisform für `Product`
1
Umfrage/Quiz
Leseplan, Level 22, Lektion 4
Nicht verfügbar
Leseplan
Optimierung von Lesevorgängen und Abfragen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION