CodeGym /Kurse /Spring Data JPA /Auswahl der Ergebnisform in JPQL

Auswahl der Ergebnisform in JPQL

Spring Data JPA
Level 11, Lektion 3
Verfügbar

1. Die Ergebnisform als Teil des Designs

Wenn wir eine Repository-Methode schreiben, unterzeichnen wir im Grunde einen kleinen Vertrag mit dem zukünftigen Leser des Codes, oft mit uns selbst. Dieser Vertrag betrifft nicht nur den JPQL-Text, sondern auch die Erwartungen: wie viele Daten zurückgegeben werden und was zu tun ist, wenn keine vorhanden sind. Wenn Sie in der Signatur Optional<Product> schreiben, versprechen Sie „null oder einen Artikel“. Wenn Sie List<Product> schreiben, geben Sie ehrlich zu: „es können viele sein“. Und wenn Sie long zurückgeben, sagen Sie direkt: „Ich lade nichts, ich zähle nur“.

Es ist hilfreich, sich vorzustellen, dass das Repository die „Kasse“ des Ladens ist, und der Service der Käufer. Der Käufer muss nicht wissen, wie die Kasse das Wechselgeld berechnet, aber er erwartet, dass er, wenn er „ein Brötchen“ verlangt, keinen Sack Kartoffeln und eine rätselhafte Quittung über eine halbe Seite erhält. Die Ergebnisform ist genau „wie die Kasse Ihnen die Ware übergibt“: ein einzelnes Objekt, einen Korb voller Objekte oder einen Beleg mit einer Zahl. Und wenn Sie die Form nicht nach dem Sinn wählen, sondern „wie es gerade gekommen ist“, werden Sie entweder im Service Behelfslösungen schreiben oder eine Exception dort erhalten, wo niemand sie erwartet.

Um diese Idee „greifbar“ zu machen, betrachten wir den Weg einer Anfrage in der Anwendung:

flowchart TD
    S[Service] --> R[Repository method]
    R --> Q["@Query (JPQL)"]
    Q --> SQL[Generated SQL]
    SQL --> DB[(PostgreSQL)]
    DB --> M[Mapping des Ergebnisses]
    M --> R
    R --> S

Für den Service ist wichtig, was aus dem Repository zurückkommt: ein Objekt, eine Liste oder eine Zahl. Das bestimmt, wie der Service weiterarbeitet und welche Prüfungen er durchführen muss.

2. Ein Objekt: Optional<T>

Wenn Sie maximal ein Ergebnis erwarten, ist Optional<T> eine nahezu ideale Form. Sie „verschönert den Code“ nicht, sondern zwingt Sie, ehrlich die Frage zu beantworten: „Was mache ich, wenn der Datensatz nicht existiert?“. Und das ist besonders praktisch, wenn die Entität einen natürlichen eindeutigen Geschäftsschlüssel hat. In unserem Mini-Shop sind solche Schlüssel normalerweise sku bei Produkten und orderNumber bei Bestellungen. Auch wenn die Datenbank noch nicht mit der Einschränkung UNIQUE aufschreit (darauf kommen wir später), deuten das Modell und der gesunde Menschenverstand bereits darauf hin: SKU ist als eindeutig konzipiert.

Beispiel: Produkt nach SKU suchen – „null oder eins“

Hier ein minimales, „reines“ Beispiel: Wir möchten ein Product anhand der sku lesen.

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

import java.util.Optional;

// JPQL-Abfrage: Nach dem Geschäftsschlüssel sku erwarten wir maximal eine Zeile
@Query("select p from Product p where p.sku = :sku")
// Optional zwingt den aufrufenden Code, das Fehlen eines Ergebnisses explizit zu behandeln
Optional<Product> findBySkuJpql(@Param("sku") String sku);

Was hier gut ist: Die Methodensignatur sagt dem zukünftigen Leser buchstäblich alles. Wir suchen nach sku, und das Ergebnis ist entweder vorhanden oder nicht.

Zwei Ergebnisse bei Optional

Und hier beginnt die „nützliche Grausamkeit“ von Spring Data. Wenn Sie Optional<Product> deklariert haben, die Abfrage aber zwei Zeilen zurückgibt, wird Spring Data nicht die „erste beste“ auswählen (wie es manchmal schlechte Beispiele aus dem Internet tun). Es ist kein Gedankenleser, es weiß nicht, welche der beiden Zeilen „richtiger“ ist. Daher erhalten Sie normalerweise eine Exception, dass das Ergebnis nicht eindeutig ist. Das tut weh, ist aber fair: Besser sofort zu scheitern, als stillschweigend das falsche Produkt auszuliefern und dann zwei Tage nach dem Fehler zu suchen, während man den rückläufigen Merkur und Hibernate beschuldigt.

Wenn es für Sie wirklich „in Ordnung“ ist, dass es mehrere Ergebnisse geben kann, dann ist Optional nicht Ihre Wahl. Sie benötigen List.

Rückgabe von Product ohne Optional

Technisch gesehen ja, aber für einen Anfänger ist es fast immer eine Falle. In Java gibt eine Methode, die Product zurückgibt, bei fehlendem Ergebnis normalerweise null zurück. Und null ist so ein „Geschenk“ aus der Vergangenheit, das später an einer Stelle explodiert (NullPointerException), an der Sie sich nicht mehr daran erinnern, dass Sie überhaupt nach einem Produkt gesucht haben.

Wenn Sie dennoch nicht Optional möchten, sondern eine strenge „Datensatz muss existieren“, dann wird dies normalerweise nicht über das Repository, sondern über den Service gemacht: Der Service nimmt Optional und wandelt das Fehlen in einen verständlichen Domänenfehler um. Das Repository bleibt ehrlich und neutral: „Kann finden, kann nicht finden“.

3. Mehrere Objekte: List<T>

Die Listen-Ergebnisform ist nicht „wenn man zu faul zum Nachdenken ist“, sondern eine normale Situation bei den meisten Lesevorgängen. Eine Kategorie enthält viele Produkte, eine Bestellung enthält viele Positionen, aktive Produkte im Katalog können viele sein. Wenn Ihre Abfrage ihrem Sinne nach eine Menge zurückgibt, dann ist List<T> die natürliche Form. Wichtig ist, dass List in Spring Data normalerweise als leere Liste zurückgegeben wird, nicht als null, und das macht den Code entspannter: Man kann einfach isEmpty() prüfen, ohne ein „Ritual zum Schutz vor null“ zu veranstalten.

Beispiel: alle Produkte in einer Kategorie

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

import java.util.List;

// Die Abfrage gibt potenziell viele Zeilen zurück, daher ist der Ergebnistyp List
@Query("select p from Product p where p.category.code = :code")
// Wenn keine Produkte vorhanden sind, gibt Spring Data eine leere Liste zurück (nicht null)
List<Product> findProductsByCategoryCode(@Param("code") String code);

Wenn die Kategorie nicht existiert oder keine Produkte enthält, erhalten Sie eine leere Liste – und das ist in Ordnung. Kein „oh, es ist null, und ich habe vergessen“.

Beispiel: Liste der Bestellpositionen

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

import java.util.List;

// Der Textblock hilft, JPQL wie normalen Text zu lesen, besonders bei mehrzeiligen Abfragen
@Query("""
       select oi
       from OrderItem oi
       where oi.order.orderNumber = :orderNumber
       order by oi.id
       """)
// Liste, weil eine Bestellung ihrem Sinne nach mehrere Positionen hat
List<OrderItem> findItemsByOrderNumber(@Param("orderNumber") String orderNumber);

Hier habe ich einen Textblock """ ... """ verwendet, damit die Abfrage wie normaler Text lesbar ist. Wichtig ist, dass das Ergebnis eine Liste ist, weil eine Bestellung ihrem Sinne nach mehrere Positionen enthält.

Häufiger Fehler: List und get(0)

Manchmal denken Leute: „Na gut, ich gebe eine Liste zurück und nehme dann das erste Element – es ist ja nur eines.“ Das ist eine listige Art, sich selbst zu betrügen: Der Compiler schweigt, aber die Aussage des Codes wird zu einer fragilen Annahme. Wenn Sie ein einzelnes Objekt erwarten, drücken Sie das besser sofort mit Optional aus. Und wenn Sie eine Liste erwarten, arbeiten Sie damit wie mit einer Liste.

4. Aggregate: count(...) und sum(...)

Wenn Sie eine Abfrage schreiben, die eine Zahl zurückgeben soll, neigt man dazu, es „einfach“ zu machen: die Liste zu laden und in Java zu zählen. Das funktioniert, aber es ist, als würde man zum Nachbargarten mit dem Flugzeug fahren: Technisch kommt man an, aber es ist laut, teuer und die Nachbarn fangen an, Fragen zu stellen. Die Datenbank kann besser und schneller zählen und summieren als Ihre Schleife im Service, weil sie nicht den gesamten Satz an Entitäten in den Speicher laden muss.

count(...): zählen, nicht laden

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

// Die Aggregatfunktion count gibt eine Zahl zurück, daher ist der Rückgabetyp long
@Query("select count(p) from Product p where p.category.code = :code")
// Wichtig: Bei fehlenden Zeilen gibt count 0 zurück, nicht null
long countProductsInCategory(@Param("code") String code);

Hier sagen wir explizit: „Ich brauche keine Produkte, ich brauche eine Zahl.“ Die Datenbank gibt eine Zeile, eine Spalte zurück – und das war‘s. Und beachten Sie: count gibt bei fehlenden Zeilen 0 zurück, was sehr praktisch ist.

sum(...): Geldsumme und die Feinheit mit null

Bei der Summe gibt es eine wichtige Nuance: sum(...) gibt in JPQL (und SQL) bei fehlenden Zeilen oft null zurück, nicht 0. Das ist aus Sicht der Datenbankmathematik logisch, aber für einen Anfänger überraschend: Sie erwarten eine Summe von „0“ und erhalten „nichts“. Entweder Sie behandeln null sorgfältig im Code, oder Sie bitten die Datenbank, durch coalesce(...) 0 zurückzugeben.

Eine einfache Variante ist, BigDecimal zurückzugeben und sich daran zu erinnern, dass es null sein kann:

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

import java.math.BigDecimal;

// sum(...) kann null zurückgeben, wenn der Kunde keine einzige Bestellung hat
@Query("select sum(o.totalAmount) from CustomerOrder o where o.customerEmail = :email")
// Der aufrufende Code muss einen möglichen null-Wert berücksichtigen
BigDecimal sumOrdersOfCustomer(@Param("email") String email);

Im Service müsste man dann so etwas schreiben wie „wenn null – dann 0“. Ja, das ist etwas mehr Code, aber dafür kontrollieren Sie die Bedeutung.

Wenn man das Ergebnis „ohne Überraschungen“ haben möchte, kann man coalesce(...) verwenden:

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

import java.math.BigDecimal;

// coalesce wandelt null (leere Ergebnismenge) in 0 um, um Prüfungen im Service zu vermeiden
@Query("""
       select coalesce(sum(o.totalAmount), 0)
       from CustomerOrder o
       where o.customerEmail = :email
       """)
// Eine solche Methode gibt stabil einen Nicht-null-Wert zurück
BigDecimal sumOrdersOfCustomerSafe(@Param("email") String email);

Der Sinn: „Wenn die Summe nicht berechnet wurde, gib 0 zurück.“ Das macht den aufrufenden Code einfacher.

select und der Ergebnistyp: Entscheidungsmatrix

Wenn Sie die Ergebnisform wählen, wählen Sie tatsächlich, was genau im select steht. Wenn Sie select p schreiben, geben Sie Entitäten zurück. Wenn Sie select count(p) schreiben, geben Sie eine Zahl zurück. Daher ist es eine gute Gewohnheit: Zuerst formulieren, was die Methode zurückgeben soll, und erst dann JPQL schreiben. Sonst kommt es zu „Ich habe die Abfrage geschrieben und dann lange den Typ gesucht, damit es wenigstens kompiliert“ – und das ist der Weg zu Laufzeitausnahmen.

Unten finden Sie eine sehr praktische Tabelle, die hilft, den Kopf klar zu behalten:

Was das Szenario benötigt Was normalerweise im select steht Ergebnistyp der Methode Was zurückgegeben wird, wenn keine Daten vorhanden sind
Eine Entität anhand eines eindeutigen Schlüssels finden select p Optional<Product> Optional.empty()
Eine Menge von Entitäten erhalten select p List<Product> [] — leere Liste
Anzahl zählen select count(p) long /
Long
0
Geldsumme berechnen select sum(x) BigDecimal null — wenn kein coalesce(...) verwendet wird
Summe „ohne null“ berechnen select coalesce(sum(x), 0) BigDecimal 0

Und hier ein Anti-Beispiel, das harmlos aussieht, aber dem Sinn nach kaputt ist: select count(p) und Rückgabe von List<Product>. Das ist, als würde man „zähle“ schreiben und erwarten, dass einem eine Kiste mit Produkten gebracht wird.

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

// SCHLECHT: Die Abfrage gibt eine Zahl zurück, aber die Methode verspricht eine Liste von Produkten
@Query("select count(p) from Product p")
// Ein solcher Vertrag widerspricht sich selbst und wird zur Laufzeit fehlschlagen
List<Product> brokenMethod();

Dies ist der Fall, in dem der Compiler nicht rettet: Der Fehler erscheint zur Laufzeit und sieht aus wie „irgendeine Spring-Magie“. In Wirklichkeit ist es keine Magie, sondern einfach eine Inkonsistenz des Vertrags.

5. Typische Fehler bei der Wahl der Ergebnisform

Fehler Nr. 1: Optional<T> zurückgeben, wo dem Sinne nach mehrere Zeilen sein können.
Ein Anfänger wählt oft Optional, weil „es ordentlicher aussieht“, und schreibt dann eine Abfrage auf ein nicht eindeutiges Feld, z. B. den name eines Produkts. Die Datenbank gibt ehrlich mehrere Zeilen zurück, aber Spring Data kann sie nicht in „ein Objekt“ packen und stürzt mit einer Exception über ein nicht eindeutiges Ergebnis ab. Behoben wird das nicht durch einen Workaround wie „wir nehmen das erste“, sondern durch eine ehrliche Lösung: Entweder wir verschärfen die Bedingung auf eindeutig, oder wir geben eine Liste zurück.

Fehler Nr. 2: List<T> zurückgeben und sich dann verhalten, als wäre es ein einzelnes Objekt.
Die Signatur List<Product> sagt dem Leser: „es können viele sein“. Wenn dann im Service products.get(0) verwendet wird, ist das, als hätte man einen Mietvertrag für ein Haus unterschrieben und würde dann darin wie in einem Zelt wohnen. Das erste leere Ergebnis endet mit einer IndexOutOfBoundsException, nicht mit einer „schönen Abwesenheit eines Produkts“. Wenn „null oder eins“ erwartet wird, ist Optional besser.

Fehler Nr. 3: Zählen durch Laden von Entitäten (find...().size()), wenn count(...) benötigt wird.
Das ist ein Fehler nicht nur in Bezug auf die Leistung, sondern auch in Bezug auf die Bedeutung. Wenn Sie Entitäten laden, starten Sie das Objekt-Mapping, ziehen Felder herein, berühren möglicherweise Beziehungen – und das alles für eine einzige Zahl. Es ist besser, die Absicht direkt in der Abfrage auszudrücken: select count(...) und eine Methode, die long zurückgibt. Solcher Code ist einfacher zu lesen und zu warten.

Fehler Nr. 4: sum(...) verwenden und vergessen, dass das Ergebnis null sein kann.
Das häufigste Szenario: Die Methode gibt BigDecimal zurück, der Service führt total.add(...) aus, und plötzlich erscheint im Log eine NullPointerException, obwohl „wir doch nur die Summe berechnet haben“. Es ist wichtig, sich daran zu erinnern: Eine leere Ergebnismenge für sum(...) ergibt oft null. Behandeln Sie entweder null im Service als 0 oder verwenden Sie coalesce(...) in JPQL, damit das Repository immer einen Nicht-null-Wert zurückgibt.

Fehler Nr. 5: Inkonsistenz zwischen select und dem Rückgabetyp (der Vertrag widerspricht sich selbst).
Manchmal wurde die Abfrage bereits geändert, z. B. statt select p wurde select count(p) gesetzt, aber der Rückgabetyp wurde vergessen. Das sieht nach einer Kleinigkeit aus, zerstört aber die Methode vollständig – und zwar nicht beim Kompilieren, sondern zur Laufzeit. Daher ist die Disziplin einfach: Ändern Sie select – überprüfen Sie sofort den Rückgabetyp und die Bedeutung des Methodennamens.

1
Aufgabe
Spring Data JPA, Level 11, Lektion 3
Gesperrt
Ein Objekt und eine Liste als zwei verschiedene Ergebnisformen
Ein Objekt und eine Liste als zwei verschiedene Ergebnisformen
1
Aufgabe
Spring Data JPA, Level 11, Lektion 3
Gesperrt
`count(...)` und `sum(...)` als Aggregatergebnisse
`count(...)` und `sum(...)` als Aggregatergebnisse
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION