CodeGym /Kurse /Spring Data JPA /Typische derived Queries des Katalogs

Typische derived Queries des Katalogs

Spring Data JPA
Level 10, Lektion 1
Verfügbar

1. Typische Katalogfilter

Ein Katalog – das ist nicht „Tabelle product und fertig“. In einer normalen Anwendung wird der Katalog fast immer mit Filtern gelesen: „zeige aktive“, „in Kategorie X“, „enthält ‚Tee‘ im Namen“, „Preis von 10 bis 50“, „in der letzten Woche hinzugefügt“. Diese Filter sind einfach, aber genau sie entscheiden, ob Ihr data-layer im täglichen Gebrauch bequem ist oder ob Sie jedes Mal umständlich einen neuen Workaround schreiben.

In unserem Projekt shop-data-jpa setzen wir jetzt bewusst auf derived Queries: Abfragen, die Spring Data JPA aus dem Methodennamen ableitet. Das ist der „schnelle Einstieg“ in die Read-Seite: Sie müssen keinen Abfragetext schreiben, aber müssen dennoch überlegen, welchen Filter Sie gerade erstellen und wie er auf SQL-Ebene aussieht. Sonst erhalten Sie die gefährlichste Magie der Welt – „Magie, die irgendwie funktioniert, aber niemand weiß, warum“.

Um nicht nur abstrakt zu bleiben, nehmen wir an, wir haben folgende Entitäten: Product (mit Feldern sku, name, status, price, createdAt, category) und Category (mit Feldern code, name). Das genaue Mapping haben wir bereits früher erstellt – heute ist nur wichtig, wie diese Felder an Filtern beteiligt sind.

2. Felder und Typen für Abfragen

Bevor wir derived Queries schreiben, ist es hilfreich, kurz innezuhalten und sich zu erinnern: Wir schreiben Methoden basierend auf den Feldern der Entity, also müssen wir deren Namen und Typen genau kennen. Das ist wie die Suche nach einem Buch in der Bibliothek: Wenn Sie sicher sind, der Autor heißt „Lew Tolstoi“, aber in der Karte steht „L.N. Tolstoi“, werden Sie im Kreis laufen und den Bibliothekar (Spring Data) beschimpfen, obwohl Sie selbst falsch lagen.

Für die folgenden Beispiele stützen wir uns auf diese „Anker“-Typen. Der Produktstatus ist ein Enum, denn eine Zeichenkette "ACTIVE" kann im Jahr 2026 durch menschliches Versagen immer noch zu "ActIve" werden, während ein Enum wenigstens ehrlich beim Kompilieren versagt. Der Preis ist BigDecimal, weil Geld – und Katzen – double nicht mögen. Das Erstellungsdatum ist LocalDateTime, damit wir „nach/vor/zwischen“ ohne zusätzliche Berechnungen nutzen können.

Hier ist ein minimaler enum, damit in den Beispielen nicht das Gefühl aufkommt „wo kommt plötzlich ACTIVE her?“:

// Status des Produkts im Katalog – eine festgelegte Wertemenge.
public enum ProductStatus {
    DRAFT,
    ACTIVE,
    ARCHIVED
}

Nun ein wichtiger Hinweis: Wenn die Spalte in der Datenbank created_at heißt, das Java-Feld aber createdAt, dann schreiben Sie in der derived Query trotzdem CreatedAt. Spring Data betrachtet das Java-Feld, nicht den Spaltennamen. Das ist einer der häufigsten Gründe für „Warum funktioniert es nicht? Ich sehe doch die Spalte in der DB“.

3. Filter nach Status

Der Filter nach Status ist das „Hello, World“ des Katalogs. Er ist einfach, leicht zu lesen und zeigt perfekt die Hauptidee der derived Queries: Nach findBy kommt der Feldname, und Spring Data JPA baut eine Gleichheitsbedingung. Im echten Leben werden mit dem Status oft „Entwürfe“, „Archiv“, „versteckte“ Produkte und alles, was nicht in den Benutzerkatalog gelangen soll, herausgefiltert.

Beginnen wir mit einer sehr einfachen Methode in ProductRepository. Beachten Sie: Hier gibt es keine „SQL-Magie“, Sie geben nur eine Absicht vor, und SQL ist eine Folge dieser Absicht.

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Derived query: Filterung nach dem Feld Product.status (Gleichheit).
    List<Product> findByStatus(ProductStatus status);
}

Übersetzt in „SQL-Denken“ erwarten Sie etwa:

-- Erwartete Abfrageidee: Wähle Produkte mit dem angegebenen Status aus.
select * from product p
where p.status = ?

Und ein wichtiger Gedanke: Der Parameter ist kein SQL-Stück, sondern ein Wert. Keine Anführungszeichen, kein manuelles status = 'ACTIVE'. Sie übergeben ProductStatus.ACTIVE, und die Infrastruktur bildet es automatisch auf den benötigten Wert in der Abfrage ab.

Um es ganz bodenständig zu machen, fügen wir im Service eine Methode hinzu, die aktive Produkte liest. Hier diskutieren wir noch keine Transaktionen und andere Lebensfreuden – wir zeigen nur „Repository als Leseinstrument“.

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import com.example.shopdatajpa.catalog.repository.ProductRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CatalogService {
    private final ProductRepository productRepository;

    public CatalogService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public List<Product> findActiveProducts() {
        // Explizite Geschäftsregel: Nur ACTIVE nehmen.
        return productRepository.findByStatus(ProductStatus.ACTIVE);
    }
}

4. Filter nach Kategorie: property path

Die Kategorie ist der erste Ort, an dem derived Queries „intelligenter“ aussehen als einfache Feldgleichheit. Warum? Weil in Product die Kategorie normalerweise nicht als Long categoryId, sondern als normale Referenz Category category gespeichert ist. Das bedeutet, Sie können Abfragen nicht über category_id, sondern über das Geschäftsfeld der Kategorie – z.B. code – formulieren. Und hier beginnt das Spannende: der property path.

Wenn Product ein Feld category und Category ein Feld code hat, dann kann die derived Query den Pfad product.category.code durchlaufen. Im Methodennamen sieht das aus wie CategoryCode. Zuerst wirkt es seltsam, aber nach ein paar Tagen gewöhnt sich das Gehirn daran, und man beginnt, es als „einfache Navigation durch Objekte“ zu verstehen.

Hier ist eine der praktischsten Methoden im Katalog:

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Property path: Product.category.code + zusätzliche Bedingung über Product.status.
    List<Product> findByCategoryCodeAndStatus(String categoryCode, ProductStatus status);
}

Auf SQL-Ebene bedeutet dies normalerweise einen Join (explizit oder implizit, abhängig vom Mapping und der SQL-Generierung), weil code in der Kategorie-Tabelle liegt:

-- Erwartete Abfrageidee: Joine category wegen c.code und filtere nach Produktstatus.
select p.*
from product p
join category c on c.id = p.category_id
where c.code = ? and p.status = ?

Warum ist das besser als findByCategoryId(...)? Weil categoryId ein technisches Feld ist, während categoryCode fachlich ist. Mit categoryCode lebt es sich leichter im API, in Logs und bei der Fehlerdiskussion. „Ihre Produkte fehlen in der Kategorie ELECTRONICS“ klingt nach einem Produktgespräch, während „Ihre Produkte fehlen in Kategorie 42“ wie ein Schicksalsgespräch klingt. Philosophisch ähnlich, aber die Fachseite bevorzugt meist die erste Version.

Übrigens versucht ein Anfänger manchmal, etwas wie findByCategory_code(...) oder findByCategory_Code(...) zu schreiben, weil es in SQL category_code gibt. Das funktioniert nicht, weil Spring Data einen Java-typischen property path erwartet, kein Snake_case. Hier basiert alles auf den Java-Feldern und ihren Namen.

5. Suche nach Namensbestandteil

Die Suche nach einem Namensbestandteil ist ein Klassiker. Der Benutzer gibt tea, cup, gaming mouse ein, und Sie möchten Produkte erhalten, bei denen dies im Namen vorkommt. Auf SQL-Ebene erwarten Sie LIKE, auf Spring Data-Ebene das Schlüsselwort Containing. Wenn Sie zusätzlich IgnoreCase hinzufügen, erhalten Sie eine sehr „benutzerfreundliche“ Suche: egal, welche Groß-/Kleinschreibung eingegeben wurde.

Hier eine saubere Methode:

import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // LIKE-Suche im Namen: „enthält Teilstring“ + Ignorieren der Groß-/Kleinschreibung.
    List<Product> findByNameContainingIgnoreCase(String namePart);
}

Im SQL-Modell erwarten Sie etwa:

-- Erwartete Abfrageidee: Substring-Suche ohne Berücksichtigung der Groß-/Kleinschreibung.
where lower(p.name) like lower(concat('%', ?, '%'))

Und hier eine wichtige Regel für Anfänger: Sie übergeben in namePart fast immer nur tea, nicht %tea%. Spring Data „umhüllt“ die Zeichenkette selbst, wenn Sie Containing verwenden. Wenn Sie %tea% übergeben, erhalten Sie %%tea%% (was die Welt meist nicht zerstört, aber wie doppelte Panik aussieht).

Eine etwas praktischere Variante für den Katalog: gleichzeitig nach Kategorie und Namensbestandteil suchen. Sie ist immer noch lesbar:

import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Kombination von property path (CategoryCode) und LIKE-Suche im Namen.
    List<Product> findByCategoryCodeAndNameContainingIgnoreCase(String categoryCode, String namePart);
}

Wenn Sie dies laut lesen, erhalten Sie fast einen menschlichen Satz: „finde Produkte nach Kategorie-Code und nach Namen, der den Teil enthält, Groß-/Kleinschreibung ignorieren“. Ja, lang, aber solange das Gehirn nicht blutet – ist alles in Ordnung.

6. Preisspanne

Der Preisfilter ist ein typisches „Shop“-Szenario und fast immer eine Spanne: „von 10 bis 50“. Auf SQL-Ebene ist das between oder ein Paar Bedingungen >= und <=. In derived Queries ist die lesbarste Variante das Schlüsselwort Between. Und hier zeigt Spring Data wieder seinen Charakter: Sie schreiben eine Methode, die wie ein Anwendungsfall klingt, und SQL wird zum Detail.

Beispiel:

import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Spannenfilter für den Preis (normalerweise inklusive Grenzen, wie SQL BETWEEN).
    List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
}

Im SQL-Denken ist das:

-- Erwartete Abfrageidee: Preis im Bereich (inklusive Grenzen).
where p.price between ? and ?

Hier müssen Sie zwei Dinge beachten. Erstens: SQL BETWEEN schließt normalerweise die Grenzen ein, d.h. „zwischen 10 und 50“ schließt 10 und 50 ein. Zweitens: Die Reihenfolge der Parameter ist wichtig: zuerst die untere, dann die obere Grenze. Wenn Sie sie vertauschen, arbeitet die Methode korrekt, gibt aber „nichts“ zurück – und das ist ein Fall, wo der Code nicht abstürzt, aber Sie traurig sind.

Oft möchte man im Katalog den Preis mit dem Status kombinieren, um keine archivierten Produkte zu sehen. Auch das ist lesbar:

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Kombination von Bedingungen: Status + Preis im Bereich.
    List<Product> findByStatusAndPriceBetween(ProductStatus status, BigDecimal min, BigDecimal max);
}

Beachten Sie die Reihenfolge: Im Methodennamen ist sie dieselbe wie in den Parametern. Das ist keine eiserne Regel „laut Spezifikation“, aber eine eiserne Regel „für das Überleben des Teams“. Wenn es viele Parameter gibt, ist die Reihenfolge das, was vor „warum haben wir min und max vertauscht?“ schützt.

7. Filter nach Erstellungsdatum

Der Datumsfilter ist ein Filter aus der Kategorie „Admin, aber täglich“. „Zeige Produkte, die nach Montag erstellt wurden“, „zeige Neuzugänge der letzten Woche“, „zeige alles, was heute hinzugefügt wurde“. Auf SQL-Ebene ist das ein Vergleich von Timestamps, und in derived Queries verwendet man dafür bequem die Schlüsselwörter After und Before. Sie lesen sich fast wie Englisch: createdAt after X.

Beispiel:

import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Filter „neuer als angegebene Datumszeit“ (createdAt > ?).
    List<Product> findByCreatedAtAfter(LocalDateTime createdAfter);
}

SQL-Modell:

-- Erwartete Abfrageidee: Nimm Produkte, die strikt später als der angegebene Zeitpunkt erstellt wurden.
where p.created_at > ?

Praktisch immer wird dieser Filter zusammen mit dem Status verwendet, denn „neue archivierte Produkte“ klingt wie ein Rätsel. Beispiel:

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Kombination: Status + „erstellt nach Datum“.
    List<Product> findByStatusAndCreatedAtAfter(ProductStatus status, LocalDateTime after);
}

Zeit ist ein Thema, bei dem selbst Erfahrene manchmal in die Grube fallen. Wir gehen jetzt nicht auf Zeitzonen ein, aber halten wir einen wichtigen praktischen Gedanken fest: Wenn Sie „heute“ als Datum verwenden, müssen Sie dies in eine konkrete LocalDateTime umwandeln (z.B. Tagesbeginn), nicht versuchen, „mit LocalDate zu vergleichen“. Sonst suchen Sie Produkte „nach 2026-03-21“ und die Datenbank vergleicht Timestamps und tut, was für sie logisch ist, aber für Sie überraschend.

8. Kombinationen mit And

Wenn Sie And beherrschen, entsteht natürlich der Wunsch, eine „universelle“ Methode zu schreiben: Status und Kategorie und Preis und Namensbestandteil und Datum. Formal kann Spring Data versuchen, dies zu parsen. Praktisch erhalten Sie einen Methodennamen, der wie eine lange, in einer Zeile gespielte Schachpartie aussieht. Und Ihr Repository beginnt auszusehen, als schreibe es einen Roman, keinen Code.

Ein Beispiel für „noch erträglich“ haben wir bereits gesehen:

// Kombination von zwei Bedingungen – normalerweise gut lesbar.
List<Product> findByCategoryCodeAndStatus(String categoryCode, ProductStatus status);

Aber hier ein Beispiel, das aussieht wie „ich wollte helfen, habe aber versehentlich einen Zauberspruch gemacht“:

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Beispiel einer „Monster-Methode“: formal gültig, aber schwer zu lesen/aufrufen/warten.
    List<Product> findByCategoryCodeAndStatusAndPriceBetweenAndNameContainingIgnoreCaseAndCreatedAtAfter(
            String categoryCode, ProductStatus status,
            BigDecimal min, BigDecimal max,
            String namePart, LocalDateTime after
    );
}

Ja, das „funktioniert“ als Idee. Aber im Team wird es meist zum Schmerz: Solche Methoden sind schwer zu lesen, schwer aufzurufen (Parameter können leicht verwechselt werden), schwer zu erweitern. Für heute ist unser Ziel, im Bereich „kurz, offensichtlich, praktisch“ zu bleiben. Wenn Sie sich dabei ertappen, „ich werde jetzt noch ein And einfügen“, ist das ein guter Moment, innezuhalten und zu fragen: „Wird dieses Szenario überhaupt in dieser Form wirklich benötigt?“

Manchmal ist die richtige Lösung, mehrere kleine Methoden zu haben, die verschiedene klare Anwendungsfälle abdecken. Zum Beispiel eine Methode für „aktive in Kategorie“, eine für „Namenssuche“, eine für „Preisspanne“. Das sieht einfacher aus und bedient die realen Bedürfnisse des Katalogs völlig normal.

9. Typische Fehler bei derived Queries des Katalogs

Fehler Nr. 1: Spaltenname statt Java-Feldnamen verwenden.
Eine sehr häufige Situation: In der Tabelle heißt die Spalte created_at, der Student schreibt aus Gewohnheit findByCreated_AtAfter(...) oder findByCreatedAt(...), aber mit falscher Groß-/Kleinschreibung, Unterstrichen oder Namensform. In derived Queries sind ausschließlich die Java-Felder der Entity beteiligt, also createdAt. Wenn Sie unsicher sind, öffnen Sie die Entität, nicht pgAdmin.

Fehler Nr. 2: Versuchen, SQL-Muster in Containing-Parametern zu schreiben.
Wenn Sie Containing verwenden, müssen Sie normalerweise "tea" übergeben, nicht "%tea%". Wenn Sie beginnen, manuell % zu malen, ist das ein Zeichen, dass Sie gedanklich in den Modus „ich schreibe SQL“ gewechselt sind, dies aber über Parameter tun, wo es nicht nötig ist. Im besten Fall ist es nur unschön, im schlimmsten Fall beginnen Sie seltsame Kombinationen zu machen und wundern sich über das Ergebnis.

Fehler Nr. 3: min und max in Between vertauschen.
Between sieht harmlos aus, ist aber genau die Methode, die „nicht abstürzt, sondern leise eine leere Liste zurückgibt“. Wenn Sie min=50, max=10 übergeben, führt das Repository die Abfrage ehrlich aus und findet nichts. Daher ist es im Service manchmal sinnvoll, entweder die Grenzen zu validieren (Minimum nicht größer als Maximum) oder die Parameter explizit minPrice und maxPrice zu nennen, damit sie schwerer zu verwechseln sind.

Fehler Nr. 4: findByCategoryId... schreiben, wenn das Modell bereits category hat.
Wenn Sie eine normale ManyToOne-Beziehung aufgebaut haben und Product ein Feld category hat, dann führen Abfragen der Art „finde nach categoryId“ zurück zum Stil „ORM wie JDBC mit Annotationen“. Man kann es natürlich so machen, aber man verliert den Hauptvorteil des Modells: die fachlichen Felder der verknüpften Entität. findByCategoryCode... liest sich wie Fachsprache, nicht wie interne FK-Mechanik.

Fehler Nr. 5: Eine Methode für alle Lebenslagen schreiben und eine „Monster-Methode“ erhalten.
Spring Data kann zwar lange Namen parsen, aber Ihr Gehirn und das Ihrer Kollegen müssen das nicht. Wenn der Name wie ein Zauberspruch aus einem Fantasy-Roman aussieht („Im Namen aller And … erhalte das Ergebnis!“), ist das ein guter Moment, innezuhalten und zu einfacheren, verständlichen Szenarien zurückzukehren. Derived Queries sind stark, wo die Methode kurz und offensichtlich ist; wenn Sie diese Grenze überschreiten, hört das Repository auf, ein Vertrag zu sein, und wird zu einem Rätsel.

1
Aufgabe
Spring Data JPA, Level 10, Lektion 1
Gesperrt
Filter nach Genre-Code und Titelteil
Filter nach Genre-Code und Titelteil
1
Aufgabe
Spring Data JPA, Level 10, Lektion 1
Gesperrt
Preisbereich und Erstellungsdatum
Preisbereich und Erstellungsdatum
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION