CodeGym /Kurse /Hibernate deep-dive /Specification: Filter im Repository

Specification: Filter im Repository

Hibernate deep-dive
Level 10, Lektion 2
Verfügbar

1. Wann braucht man Specification statt Criteria API

Wir haben bereits dynamisches where via Criteria API zusammengestellt. Specification eröffnet keine neue Abfragesprache und löst keine andere SQL‑Aufgabe. Es ist eine repository‑freundliche Form derselben Idee: die gleichen Criteria‑Prädikate, nur so verpackt, dass ein Spring‑Data‑Repository sie kombinieren und ausführen kann.

Wenn Criteria API bereits eine Abfrage ohne String‑Verklebung bauen kann, liegt die logische Frage nahe: „Was brauchen wir dann noch?“ Die Antwort ist sehr alltagstauglich: Wir brauchen nicht nur eine einzige Abfrage, sondern wir wollen vermeiden, dass Filter per Copy‑Paste im ganzen Projekt vervielfältigt werden. Und dass Sie nach einem Monat noch verstehen, warum „Produktsuche“ an einer Stelle nach Status filtert und an einer anderen nicht.

Der grundlegende Anwendungsfall bleibt die Backoffice‑Suche nach Bestellungen. Aber die Mechanik der komponierbaren Filter lässt sich besser zuerst an Produkten zeigen: dort gibt es weniger Ablenkung, und man sieht besser, wie ein einzelner Filter zu einem kleinen wiederverwendbaren Baustein wird.

Stellen Sie sich einen typischen Backoffice‑Fall im Commerce Persistence Lab vor: „Zeige Produkte, deren Status = ACTIVE, deren Name „pro“ enthält und deren SKU mit „SKU-“ beginnt.“ Heute so, morgen kommt ein Filter für den Preisbereich hinzu, übermorgen „nur Produkte, die in Bestellungen der letzten Woche vorkommen“ (ja, das klingt nach viel Arbeit – und das ist normal). Wenn Sie die Criteria‑Abfrage direkt im Service schreiben, bekommen Sie schnell eine lange Methode, in der die Hälfte der Zeilen aus if (...) filters.add(...) besteht.

Specification ist eine Möglichkeit, Filter benannt, wiederverwendbar und kombinierbar zu machen. Im Idealfall soll der Service die Abfrage auf Bedeutungsebene zusammenstellen: „es gibt einen Status“, „der Name beginnt mit“, „Kunde mit dieser E‑Mail“. Die technische Mechanik von Criteria bleibt in kleinen Funktionen verborgen.

Um den Unterschied zu spüren, kann man sich den „naiven“ Stil vorstellen. Machen Sie das nicht im Produktivcode (und eigentlich gar nicht), aber als Anti‑Beispiel ist es nützlich:

import java.util.ArrayList;
import java.util.List;

public class AntiExample {

    // Anti‑Beispiel: so beginnt meist eine „dynamische Abfrage auf dem Knie“
    public void pleaseNo() {
        // Hier fangen Leute oft an, where‑Bedingungen als Strings zu sammeln
        // oder die Methode mit if's aufzublähen
        List<String> where = new ArrayList<>();

        // Irgendwo hier würden Sie anfangen, einen JPQL‑String zu verkleben
        // oder 30 if's in Criteria zu stapeln
        // und dann kommt ein weiterer Filter hinzu und Sie werden diese Zeile verfluchen
    }
}

Specification hilft nicht, weil sie „schneller“ oder „schlauer“ wäre. Sie hilft, weil sie den Code wartbar macht: Filter werden zu kleinen Bausteinen und die Abfragekomposition zu einer sauberen Zusammenstellung.

2. Specification<T>: Idee und Aufbau

Das Wort Specification klingt nach etwas aus der Welt der „Architektur‑Komitees und großen Präsentationen“, aber in der Praxis ist es eine sehr bodenständige Sache. Specification<T> ist ein funktionales Interface, das ein Predicate (eine Bedingung) für eine Criteria‑Abfrage erzeugen kann. Es ist buchstäblich ein „Stück where“, das Sie mit anderen Stücken über and() und or() kombinieren können.

In Spring Data JPA sieht das so aus: die Specification erhält drei Parameter – Root<T>, CriteriaQuery<?> und CriteriaBuilder. Das sind dieselben Akteure, die wir bereits im Criteria‑API gesehen haben. Statt alles an einer Stelle zu schreiben, verpacken Sie einen einzelnen Filter in eine eigene Funktion.

Ein minimales Beispiel einer Specification für Product könnte so aussehen:

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.catalog.entity.Product;

public class SpecExample {

    public static Specification<Product> skuEquals(String sku) {
        // Gibt eine Specification zurück: Eingabe sind Criteria‑Objekte, Ausgabe Predicate für WHERE
        return (root, query, cb) -> cb.equal(root.get("sku"), sku);
    }
}

Eine wichtige Nuance für reale Projekte: Filter sind fast immer optional. Hier kommt ein nettes Feature von Specification ins Spiel: eine Specification (oder ihr Predicate) kann null sein. Spring Data behandelt das bei der Komposition so, als wäre der Filter nicht vorhanden und würde einfach übergangen. Daher schreibt man im echten Code fast immer so, dass die Specification bei null/blank Eingabeparametern null zurückgibt.

Manchmal wird statt null cb.conjunction() zurückgegeben. Vom Sinn her ist das dasselbe „kein Filter“, nur über ein neutrales Predicate. Wichtig ist, das nicht als ein anderes Modell von Specification zu betrachten: es sind lediglich zwei verschiedene Wege, das Fehlen einer Bedingung auszudrücken.

Hier eine realistischere Version:

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.catalog.entity.Product;

public class ProductSpecs {

    public static Specification<Product> skuEquals(String sku) {
        // Wenn der Filter nicht gesetzt ist – null zurückgeben, Spring Data „überspringt“ ihn bei der Komposition
        if (sku == null || sku.isBlank()) {
            return null;
        }

        // Andernfalls wird die Bedingung gebaut: product.sku = :sku
        return (root, query, cb) -> cb.equal(root.get("sku"), sku);
    }
}

Und ja, an dieser Stelle kann man sich eine leichte Selbstironie erlauben: Specification ist der Moment, in dem man endlich aufhört, eine Abfrage wie ein Sandwich aus Strings, Klebeband und Hoffnung zu bauen, und sie stattdessen wie ein normaler Mensch aus Teilen zusammensetzt, von denen jedes eine einzige Sache tut.

3. JpaSpecificationExecutor im Repository

In Spring Data JPA ist eine Specification für sich genommen nur ein Objekt. Damit sie praktisch wird, muss das Repository sie ausführen können. Dazu erweitert man das Repository nicht nur mit JpaRepository, sondern auch mit JpaSpecificationExecutor. Dann stehen Methoden wie findAll(spec), findAll(spec, pageable), count(spec) und sogar exists(spec) zur Verfügung.

In unserem Projekt sieht das maximal einfach aus. Zum Beispiel für den Katalog:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import com.example.commerce.catalog.entity.Product;

public interface ProductRepository
        // JpaSpecificationExecutor fügt Methoden wie findAll(spec), count(spec) etc. hinzu
        extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {
}

Und für Bestellungen:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import com.example.commerce.orders.entity.PurchaseOrder;

public interface PurchaseOrderRepository
        // Gleiche Idee: Das Repository kann jetzt Specifications ausführen
        extends JpaRepository<PurchaseOrder, Long>, JpaSpecificationExecutor<PurchaseOrder> {
}

Danach haben Sie genau das, wofür das Ganze gedacht ist: die Möglichkeit, Filter separat zu beschreiben (in einer ...Specifications-Klasse) und im Service eine lesbare Komposition zu bauen. Das ist besonders nützlich in den Labszenarien des Kurses, wo wir ständig vergleichen, „wie SQL aussieht“ und „warum es mehr/weniger Abfragen wurden“, und wir Filter schnell ändern müssen, ohne Abfragen manuell umzuschreiben.

Und noch eine Disziplin: JpaSpecificationExecutor ist keine Einladung, „die gesamte Geschäftslogik ins Repository zu stopfen“. Das Repository bleibt der Ort, an dem die Abfrage lebt, nicht der Ort, an dem über Rabatte, Bestellstatus und den Sinn des Lebens entschieden wird.

4. Kleine Specifications: ein Filter – eine Funktion

Der gesündeste Stil für Specification ist, wenn jede Specification für einen Filter zuständig ist und der Methodenname sich wie eine menschliche Phrase liest. Dann wird der Code nicht zu „spec1/spec2/spec3“, sondern sieht aus wie ein normaler Satz: hasStatus(ACTIVE).and(nameStartsWith("pro")).

Legen wir eine Utility‑Klasse (oder eine Sammlung von Utility‑Klassen) im Katalog an. Logischerweise im Paket com.example.commerce.catalog.query, weil es ein read‑orientierter Teil ist, keine Domänenmodell.

Beispiel – Filter nach Status:

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.catalog.entity.Product;
import com.example.commerce.catalog.entity.ProductStatus;

public final class ProductSpecifications {

    public static Specification<Product> hasStatus(ProductStatus status) {
        // Optionaler Filter: wenn der Status nicht gesetzt ist – keine Specification
        if (status == null) {
            return null;
        }
        // WHERE product.status = :status
        return (root, query, cb) -> cb.equal(root.get("status"), status);
    }
}

Filter „Name beginnt mit Präfix“ (und gleich case‑insensitive, weil Benutzer sich nicht an die Groß‑/Kleinschreibung erinnern müssen und wir nicht leiden müssen):

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.catalog.entity.Product;

public final class ProductNameSpecifications {

    public static Specification<Product> nameStartsWithIgnoreCase(String prefix) {
        // Leerer Präfix => kein Filter
        if (prefix == null || prefix.isBlank()) {
            return null;
        }

        // LIKE 'pro%' in Kleinbuchstaben (case‑insensitive)
        String like = prefix.toLowerCase() + "%";
        return (root, query, cb) -> cb.like(cb.lower(root.get("name")), like);
    }
}

Filter nach SKU‑Präfix:

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.catalog.entity.Product;

public final class ProductSkuSpecifications {

    public static Specification<Product> skuStartsWith(String prefix) {
        // Leerer SKU => kein Filter
        if (prefix == null || prefix.isBlank()) {
            return null;
        }

        // WHERE sku LIKE 'SKU-%'
        return (root, query, cb) -> cb.like(root.get("sku"), prefix + "%");
    }
}

Beachten Sie: Wir versuchen nicht, „sofort eine universelle Specification für alles“ zu schreiben. Das ist eine häufige Anfängerfalle: eine einzige riesige Methode buildSpecification(filter) mit 200 Zeilen zu schreiben und sich zu freuen, dass „alles an einem Ort ist“. Nach ein paar Wochen fängt dieser Ort an, wie ein vergessener Behälter im Kühlschrank zu riechen – technisch immer noch ein Behälter, aber das Öffnen ist beängstigend.

Es ist besser, Specifications klein zu halten und die Komposition getrennt zu lassen.

5. Komposition: where, and, or und null

Der coolste Teil von Specification ist die Komposition. Sie können eine Abfrage aus Filtern wie aus Bausteinen zusammensetzen. Und das ist keine Metapher „für die Schönheit“: es ist buchstäblich spec1.and(spec2).or(spec3). Und wenn ein Filter nicht gesetzt ist, geben Sie null zurück, und die Komposition bleibt sauber.

Definieren wir ein kleines Filterobjekt. In den Vorlesungen über Projektionen haben wir uns bereits daran gewöhnt, dass das Ausgabeformat wichtig ist; hier fixieren wir auch die „Eingabeform“ für die Suche.

import com.example.commerce.catalog.entity.ProductStatus;

// DTO für Eingabeparameter der Suche (das, was im Service ankommt)
public record ProductSearchFilter(
        ProductStatus status,
        String namePrefix,
        String skuPrefix
) {
}

Jetzt der Lese‑Service (read‑orientiert). Nennen wir ihn z. B. ProductSearchService und machen eine Read‑Only‑Transaktion – so halten wir diszipliniert die Query‑Ausführung in einer normalen Grenze.

import java.util.List;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.commerce.catalog.entity.Product;
import com.example.commerce.catalog.repository.ProductRepository;

@Service
public class ProductSearchService {

    private final ProductRepository productRepository;

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

    @Transactional(readOnly = true)
    public List<Product> search(ProductSearchFilter filter) {
        // Abfrage aus kleinen „Bausteinen“ zusammensetzen; null‑Specs werden übersprungen
        Specification<Product> spec = Specification
                .where(ProductSpecifications.hasStatus(filter.status()))
                .and(ProductNameSpecifications.nameStartsWithIgnoreCase(filter.namePrefix()))
                .and(ProductSkuSpecifications.skuStartsWith(filter.skuPrefix()));

        // JpaSpecificationExecutor führt die Criteria‑Abfrage für uns aus
        return productRepository.findAll(spec);
    }
}

In diesem Beispiel stecken zwei Ideen, die es wert sind, im Kopf behalten zu werden.

Die erste Idee: Der Service stellt die Abfrage auf Bedeutungsebene zusammen. Sie lesen where(hasStatus).and(nameStartsWith).and(skuStartsWith) und verstehen ungefähr, was im SQL stehen wird, ohne das Log zu öffnen. Das ist ein großer Schritt in Richtung „vorhersagbare Persistence Layer“.

Die zweite Idee: Specification.where(...) ist ein praktischer Startpunkt. Sie können sogar mit where(null) beginnen (wenn Ihnen das leichter fällt) und dann and() hinzufügen. Aber meist ist es psychologisch angenehmer, mit dem ersten Filter zu starten: weniger das Gefühl, „wir werden jetzt zaubern“.

Ein Beispiel für or() – nehmen wir an, wir möchten eine Freitextsuche: „Suche entweder im Namen oder im SKU“. Ja, eigentlich geht das in Richtung Volltextsuche, aber als Lehrbeispiel ist es perfekt.

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.catalog.entity.Product;

public final class ProductTextSearchSpecifications {

    public static Specification<Product> nameOrSkuContainsIgnoreCase(String q) {
        // Leere Suchanfrage => kein Filter
        if (q == null || q.isBlank()) {
            return null;
        }

        // '%q%' in Kleinbuchstaben, damit der Vergleich case‑insensitive ist
        String like = "%" + q.toLowerCase() + "%";

        // (lower(name) like :like) OR (lower(sku) like :like)
        return (root, query, cb) -> cb.or(
                cb.like(cb.lower(root.get("name")), like),
                cb.like(cb.lower(root.get("sku")), like)
        );
    }
}

Und im Service fügen Sie einfach .and(nameOrSkuContainsIgnoreCase(filter.q())) hinzu. Das ergibt Dynamik ohne Strings, ohne Verklebung, ohne Tränen und ohne „warum ist hier ein überflüssiges Leerzeichen und die Abfrage stürzt nur im Produktivbetrieb ab“.

6. Join in Specification: Filterung über Beziehungen

Bei den Produkten war es einfacher, die Komposition ohne überflüssigen Ballast zu sehen. Bei den Bestellungen wird deutlich, wofür diese Technik in der üblichen Backoffice‑Suche benötigt wird: Ein Teil der Bedingungen lebt auf Beziehungen, und join() wird genau für die Filterung benötigt.

Sehr schnell entsteht die nächste Komplexitätsebene: Der Filter bezieht sich nicht auf ein Feld der Wurzelentität, sondern auf eine Beziehung. Zum Beispiel „Bestellungen eines Kunden mit einer bestimmten E‑Mail“ oder „Bestellungen mit Status X, die eine Position mit einer bestimmten SKU enthalten“. Und hier bleibt Specification praktisch, aber es entsteht eine wichtige Disziplin: join() in einer Specification dient in erster Linie der Filterung, nicht dem „Laden des gesamten Graphen“.

Beginnen wir mit einem einfachen Fall: Suche nach Bestellungen anhand der Kunden‑E‑Mail. In der Domäne des Projekts ist PurchaseOrder mit Customer über ManyToOne verbunden, das ist die natürliche Navigation.

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.orders.entity.PurchaseOrder;

public final class OrderSpecifications {

    public static Specification<PurchaseOrder> customerEmailEquals(String email) {
        // Keine E‑Mail angegeben => kein Filter
        if (email == null || email.isBlank()) {
            return null;
        }

        // JOIN customer und Filterung über customer.email
        return (root, query, cb) ->
                cb.equal(root.join("customer").get("email"), email);
    }
}

Jetzt der Service (kurz, ohne überflüssigen Ballast):

import java.util.List;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.commerce.orders.entity.PurchaseOrder;
import com.example.commerce.orders.repository.PurchaseOrderRepository;

@Service
public class OrderSearchService {

    private final PurchaseOrderRepository repository;

    public OrderSearchService(PurchaseOrderRepository repository) {
        this.repository = repository;
    }

    @Transactional(readOnly = true)
    public List<PurchaseOrder> findByCustomerEmail(String email) {
        // Eine Specification – ein Filter; dann über das Repository ausführen
        Specification<PurchaseOrder> spec = OrderSpecifications.customerEmailEquals(email);
        return repository.findAll(spec);
    }
}

Jetzt der „interessante“ Fall: Filter über eine Kollektion, z. B. „Bestellungen, die eine Position mit einer bestimmten SKU enthalten“. Normalerweise macht man einen Join auf items, dann einen Join auf product und einen Vergleich auf sku. Und hier taucht fast immer das Thema Dubletten auf: Eine Bestellung kann mehrere Positionen haben, und der Join liefert mehrere Ergebniszeilen pro Wurzel. Deshalb braucht man oft query.distinct(true).

import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.orders.entity.PurchaseOrder;

public final class OrderItemSpecifications {

    public static Specification<PurchaseOrder> hasItemWithProductSku(String sku) {
        // Leere SKU => kein Filter
        if (sku == null || sku.isBlank()) {
            return null;
        }

        return (root, query, cb) -> {
            // Wichtig: Join auf eine Kollektion kann Root‑Zeilen vervielfachen, distinct behebt das
            query.distinct(true);

            // JOIN items -> product und Filterung über product.sku
            return cb.equal(
                    root.join("items").join("product").get("sku"),
                    sku
            );
        };
    }
}

Und hier ist es wichtig, zwei Begriffe nicht zu verwechseln. join(...) in einer Specification bedeutet nicht, dass Sie „das Problem des Lazy Loading gelöst“ haben oder „alles sofort geladen“ wird. Es ist ein Join zur Bedingungen. Der Fetch‑Plan und der Kampf gegen N+1 ist eine separate Disziplin, die Sie bereits an den vorherigen Tagen kennengelernt haben (JOIN FETCH, EntityGraph, Batch Fetching). Wenn Sie anfangen, Fetching über Specification zu lösen, bekommen Sie leicht Abfragen, die sich bei Paginierung unvorhersehbar verhalten und insgesamt schwerer lesbar sind.

7. Specification + Pageable/Sort: count und distinct

Wenn Sie eine Backoffice‑Suche machen, brauchen Sie fast immer Paginierung und Sortierung. Hier kombiniert sich Specification hervorragend mit Pageable: Das Repository baut automatisch die Hauptabfrage und eine separate Count‑Abfrage, um die Anzahl der Elemente zu zählen. Das ist praktisch, aber manchmal unerwartet teuer, besonders wenn Sie anfangen, Kollektionen zu joinen und distinct zu setzen.

Ein klassischer Aufruf sieht so aus:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.orders.entity.PurchaseOrder;

public class PageExample {

    public Page<PurchaseOrder> page(PurchaseOrderRepository repo, Specification<PurchaseOrder> spec) {
        // Pageable gibt Seitennummer und Größe an; Spring Data macht normalerweise select + count
        var pageable = PageRequest.of(0, 20);
        return repo.findAll(spec, pageable);
    }
}

Wenn die Specification einen Join auf eine Kollektion und query.distinct(true) enthält, müssen Sie verstehen: distinct beeinflusst nicht nur den „Haupt‑Select“, sondern kann auch den Count beeinflussen. Manchmal ist das nötig (sonst wäre der Count überhöht), manchmal macht es den Count schwerer. In einem Übungsprojekt ist das ein guter Moment, um zu lernen, Count‑Abfragen nicht zu „fürchten“, sondern das SQL‑Log zu betrachten und zu verstehen, warum sie auftauchen.

Eine weitere Nuance ist die Sortierung. Wenn Sie nach einem Feld der Root‑Entität sortieren (createdAt, orderNumber), ist alles einfach. Wenn Sie nach einem Feld einer verknüpften Entität sortieren möchten, brauchen Sie einen Join (oder einen anderen Abfragestil). Specification erlaubt das, aber die Lesbarkeit kann stark leiden. Das ist kein Verbot, sondern einfach der technische Preis: Je „dynamischer und universeller“ die Suche, desto wahrscheinlicher wird die Abfrage unübersichtlich.

Um die Vorlesung nicht in einen eigenen Kurs „Wie baue ich einen perfekten Search‑Endpoint für alle Lebenslagen“ zu verwandeln, halten wir eine praktische Regel fest: Specification ist großartig, wenn die Dynamik im where lebt. Wenn die Dynamik anfängt, in select, group by, Aggregaten und Berichts‑Dingen zu leben, ist die Specification nicht mehr das klarste Werkzeug. Und das wird in den folgenden Vorlesungen des Tages sehr nützlich sein.

8. Specification und Projektionen: Wie man nichts Überflüssiges lädt

Nach dem Tag über Projektionen haben Sie wahrscheinlich schon verinnerlicht: Die Produktliste für das Admin‑Panel ist oft nicht „Product komplett zurückgeben“, sondern „eine schmale Zeile zurückgeben“. Und hier macht man leicht einen Schritt zurück: JpaSpecificationExecutor gibt standardmäßig List<Product> zurück, und Sie laden wieder eine Managed Entity, obwohl Sie ein Read‑Model wollten. Das ist kein Verbrechen, aber ein technischer Kompromiss, den man sehen sollte.

Für die Bestellliste bedeutet das eine einfache Sache: Das dynamische where kann in Specification bleiben, und nach außen geben Sie ein schmales OrderRow zurück, ohne PurchaseOrder als Read‑Model zu schleppen.

Die gute Nachricht: In Spring Data JPA gibt es die Fluent‑Methode findBy(spec, queryFunction), die es erlaubt, eine Projektion direkt zusammen mit der Specification anzuwenden. Das sieht auf den ersten Blick etwas „magisch“ aus, aber vom Sinn her ist alles ehrlich: Die Specification bildet das where, und die queryFunction sagt, „in welcher Form das Ergebnis zurückgegeben wird“.

Machen wir ein einfaches Read‑Model für die Produktliste:

import com.example.commerce.catalog.entity.ProductStatus;

// Schmales Read‑Model: nur das, was für die Tabelle/Liste benötigt wird
public record ProductRow(
        long id,
        String sku,
        String name,
        ProductStatus status
) {
}

Jetzt ein Beispielaufruf: Wir wollen nicht List<Product>, sondern List<ProductRow>.

import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

import com.example.commerce.catalog.entity.Product;

public class ProjectionWithSpecExample {

    public List<ProductRow> rows(ProductRepository repo, Specification<Product> spec) {
        // Specification gibt WHERE vor, dann sagen wir, mit welcher Projektion das Ergebnis zurückkommt
        return repo.findBy(spec, q -> q
                .as(ProductRow.class)
                .sortBy(Sort.by("name"))
                .all());
    }
}

Beachten Sie, wie sehr das die Disziplin des vorherigen Tages beibehält. Wir schleppen die Entity immer noch nicht als Read‑Model, verkleben aber keinen JPQL‑String und schreiben kein Criteria‑API an jeder Stelle. Für die Backoffice‑Suche ist das oft der goldene Mittelweg: dynamisches where plus schmale Ausgabe.

Hier gibt es eine technische Grenze. Projektion + Specification eignet sich hervorragend, wenn Sie eine „flache“ Zeile lesen: Felder einer Entität oder einfache Felder einer to‑one‑Beziehung (je nachdem, wie Spring Data das matchen kann). Wenn Sie aber eine komplexe Ausgabe, Aggregate oder ein Berichtsformat möchten, bewegen Sie sich in eine Zone, in der JPQL (mit Constructor‑Expression), CriteriaQuery „von Hand“ oder Native SQL (etwas später heute) einfacher und ehrlicher sind.

9. Typische Fehler beim Arbeiten mit Specification

Fehler Nr. 1: Eine riesige Specification „für alles“ statt kleiner wiederverwendbarer Filter.
Man möchte ganz gern eine buildSpec(filter) mit 150 Zeilen schreiben, weil es „schneller geht“ und „alles an einem Ort ist“. Meist endet das damit, dass man sich fürchtet, da hineinzugehen: Jede Änderung zerbricht etwas Unerwartetes, und Code‑Reviews werden zur Archäologie. Specifications funktionieren gut als kleine Bausteine, die im Service oder in einem separaten Builder kombiniert werden.

Fehler Nr. 2: Vermischung von Geschäftslogik und Filterlogik an einer Stelle.
Specification geht es um die Abfrage, nicht um Domänenregeln. Sobald innerhalb einer Specification Bedingungen wie „wenn die Bestellung den Status PAID hat, dann noch dies und das“ auftauchen, riskieren Sie, eine Geschäftsentscheidung im Repository zu verstecken. Später ändert jemand die Regel im Service und vergisst, dass sie auch „eingebaut“ in der Specification steckt – das ergibt einen doppelten Boden.

Fehler Nr. 3: Der Versuch, Fetching und N+1 über Specification zu heilen.
Manchmal denkt man: „Na, wenn ich schon einen Join in der Specification mache, dann lade ich doch gleich alles.“ Technisch möglich, aber es verwandelt die Specification schnell in einen schwer lesbaren Kombinat, und das Verhalten bei Paginierung wird gefährlich. Der Fetch‑Plan ist eine separate Geschichte: dafür gibt es JOIN FETCH, EntityGraph, Batch‑Fetching. Die Specification ist in erster Linie für das where da.

Fehler Nr. 4: Vergessen von Dubletten beim Join auf eine Kollektion und kein distinct gesetzt.
Ein Filter wie „Bestellungen, die eine Position mit dieser SKU enthalten“ macht fast immer einen Join auf items. Ohne distinct kann es passieren, dass Sie denselben PurchaseOrder mehrfach erhalten, und das sieht dann aus, als ob „das Repository spinnt“. Das Repository spinnt nicht – es ist die SQL‑Form des Ergebnisses. Wenn der Join Dubletten der Root‑Zeilen erzeugt, müssen Sie entweder distinct setzen oder die Abfrageform überdenken.

Fehler Nr. 5: Nicht verstehen, dass Paginierung fast immer zwei Abfragen bedeutet.
Wenn Sie findAll(spec, pageable) aufrufen, macht Spring Data normalerweise einen Select und einen Count. Das ist normal und erwartet. Wenn Sie sich über die Count‑Abfrage „wundern“ und versuchen, sie „magisch auszuschalten“, kämpfen Sie gegen das Framework. Der richtige Weg ist, das SQL zu sehen, seine Kosten zu verstehen und die Abfragen einfach genug zu halten, damit der Count kein separates Problem wird.

1
Aufgabe
Hibernate deep-dive, Level 10, Lektion 2
Gesperrt
Produktsuche mittels Specification und `and`-Komposition
Produktsuche mittels Specification und `and`-Komposition
1
Aufgabe
Hibernate deep-dive, Level 10, Lektion 2
Gesperrt
Freie Produktsuche mit Specification und or-Komposition
Freie Produktsuche mit Specification und or-Komposition
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION