CodeGym /Kurse /Spring Data JPA /Scrolling: Window und Repository-Cleanup

Scrolling: Window und Repository-Cleanup

Spring Data JPA
Level 16, Lektion 4
Verfügbar

1. Paginierung vs. Scrolling

Nach Optional, List, Page und Slice scheint es, dass die Leseformen bereits abgeschlossen sind. Normalerweise reicht das auch aus. Scrolling kommt in einem anderen Szenario ins Spiel: wenn man über einen längeren Zeitraum hinweg sequenziell durch eine große Datenmenge gehen muss, anstatt einem Menschen „Seite 7 von 12“ zu zeigen. Es ist kein neuer Standard für normale Katalogmethoden, sondern ein eigener Vertrag für einen langen Lese-Anwendungsfall.

Paginierung bleibt die normale Wahl für die Benutzeroberfläche. Aber wenn die Datenmenge sehr groß ist oder einfach Fenster für Fenster verarbeitet werden soll, verhält sich LIMIT/OFFSET wie ein alter Aufzug: Es fährt zwar, aber je höher man steigt, desto deutlicher wird der Preis. Dann bietet Spring Data Scrolling an: das Lesen in Portionen über eine Fortsetzungsposition, nicht über eine Seitennummer.

Der neue Unterschied ist einfach: Statt Page<T> oder Slice<T> erscheint Window<T>, und statt pageNumber — ScrollPosition. Das heißt, Sie fragen nicht „Gib mir Seite Nummer 5“, Sie sagen „Lies weiter nach dieser Position“.

Zum Vergleich reicht dieser Spickzettel:

Rückgabetyp Was Sie erhalten Wann ist das sinnvoll
List<T> die gesamte Liste auf einmal kleine Nachschlagewerke und kurze Abfragen
Page<T> Datenteil + Gesamtanzahl wenn die Benutzeroberfläche tatsächlich mit Seiten lebt und die Gesamtanzahl sehen will
Slice<T> Datenteil + Kennzeichen „gibt es noch mehr“ wenn ein „zeig mehr“ benötigt wird, aber die Gesamtanzahl unwichtig ist
Window<T> Datenfenster + Fortsetzungsposition wenn eine große Datenmenge sequenziell, Fenster für Fenster, durchlaufen werden muss

Wichtig: Scrolling kann offset-basiert oder keyset-basiert sein. Offset denkt immer noch in Begriffen wie „N Zeilen überspringen“, keyset setzt das Lesen nach dem letzten Sortierschlüssel fort. Für große Datenmengen ist keyset in der Regel stabiler, aber in beiden Fällen benötigen Sie eine stabile Reihenfolge.

2. ScrollPosition: Fortsetzungsposition und ihr „exklusiver“ Charakter

Das Wichtigste beim Scrolling ist nicht Window, sondern die richtige Einstellung zur Position. ScrollPosition ist keine „Seitennummer“ und nicht einmal unbedingt eine „Zeilennummer“. Es ist genau die Position, von der aus fortgesetzt werden soll, also ein Analogon zu einem Lesezeichen in einem Buch: „Lies weiter nach dieser Stelle“. Und hier gibt es eine Feinheit, die Anfängern fast so zuverlässig das Leben schwer macht wie null in Java: Scrolling ist von Natur aus exklusiv.

Was bedeutet exklusiv in menschlichen Worten: Wenn Ihre Position auf das Element X zeigt, beginnt die nächste Abfrage nach X, nicht mit X. Aus diesem Grund sind ScrollPosition.offset() und ScrollPosition.offset(0L) nicht dasselbe.

Ein Mini-Beispiel, damit Sie nicht fragen „warum habe ich immer minus ein Produkt?“:

import org.springframework.data.domain.ScrollPosition;

// Start des Scroll-Vorgangs: „zeigen noch auf nichts“, überspringen nichts
ScrollPosition start = ScrollPosition.offset();

// Position „auf dem ersten Element“ (0), aber Scrolling ist exklusiv,
// daher beginnt die nächste Abfrage NACH ihm (also tatsächlich mit dem 2.)
ScrollPosition atZero = ScrollPosition.offset(0L);

Das zweite wichtige Ding: ScrollPosition hat einen „Modus“. Sie können entweder als offset-Scrolling oder als keyset-Scrolling starten. Im keyset-Modus speichert die Position einen Satz von Schlüsseln für die Fortsetzung, und dafür ist eine stabile Sortierreihenfolge erforderlich.

Für unser Übungsprojekt ist das sogar angenehm: id der Entität ist immer non-null, eindeutig und eignet sich hervorragend als „Anker“ der Sortierung. Das heißt, für ein Scroll-Szenario ist die natürliche Wahl ORDER BY id ASC. Wenn die Sortierung auf Feldern aufbaut, in denen null möglich ist, verhält sich die Reihenfolge deutlich weniger vorhersagbar.

3. Window<T>: Portion der Ergebnisse und Navigation

Wenn ScrollPosition ein Lesezeichen ist, dann ist Window<T> eine „Buchseite“, aber nicht im Sinne einer Seitenzahl, sondern im Sinne von „hier ist eine Portion Text, die du gerade liest“. In Window gibt es die Ergebniselemente selbst und die Möglichkeit zu erkennen, ob das Ergebnis beendet ist und wo sich die Position befindet, um das Lesen fortzusetzen. Und Hauptsache: Window ist nicht Page. Es verspricht Ihnen keinen total count und erzeugt nicht die Illusion „ich weiß, wie viele Seiten ich insgesamt haben werde“.

Der Hauptsinn hier ist einfach: Window existiert, um das „portionierte Lesen“ bequem zu machen und den Service nicht in eine manuelle SQL-Komposition mit where id > ... an jeder zweiten Stelle zu verwandeln.

Wenn man die Scrolling-Schleife visualisiert, ergibt sich etwa Folgendes:

flowchart TD
    A["ScrollPosition: Start (offset/keyset)"] --> B["Repository-Methode gibt Window<T> zurück"]
    B --> C{"window.isLast()"}
    C -- "nein" --> D["Hole Fortsetzungsposition: window.positionAt(lastIndex)"]
    D --> B
    C -- "ja" --> E["Stop: gesamtes Ergebnis gelesen"]

In diesem Moment ist es wichtig, das richtige Modell beizubehalten: Sie springen nicht „auf Seite 17“, sondern lesen sequenziell „Fenster für Fenster“. Das eignet sich hervorragend für Szenarien wie „die gesamte Datenmenge durchlaufen“, ist aber für eine Benutzeroberfläche mit Schaltflächen „Seite 3“ in der Regel schlechter, weil der Benutzer dort eine Nummerierung und vorhersagbaren wahlfreien Zugriff benötigt.

Und noch eine Nuance, die heute wie ein Refrain klingen wird: Scrolling erfordert eine stabile Reihenfolge. Daher möchten Sie fast immer so etwas wie OrderByIdAsc. Wenn die Reihenfolge undefiniert oder nicht eindeutig ist, riskieren Sie, Duplikate oder Lücken zwischen den Fenstern zu erhalten. Besonders lustig (nein) ist das, wenn zwischen zwei Fenstern jemand neue Zeilen in die Mitte der Sortierung eingefügt hat.

4. Scrolling in shop-data-jpa

Jetzt machen wir einen kleinen, aber aussagekräftigen Schritt in unserem shop-data-jpa: Wir fügen das Scrolling-Lesen von Produkten hinzu. Das ist nicht die Hauptfunktion des Shops, sondern einfach ein gutes Szenario, an dem man sieht, dass die Repository-API nicht nur „gib mir eine Katalogseite“ sein kann, sondern auch „gib mir Datenportionen für langes sequenzielles Lesen“.

Stellen wir uns einen einfachen Anwendungsfall vor: Wir müssen alle aktiven Produkte durchgehen, um zum Beispiel eine interne Liste von IDs für die weitere Verarbeitung zu erstellen (Export, Neuberechnung des Schaufensters, Datenvorbereitung – egal). Wir wollen kein findAll() und nicht „alles auf einmal“ im Speicher halten. Wir wollen in Portionen lesen.

Hinzufügen einer separaten Scrolling-Methode zu ProductRepository

Für einen solchen Anwendungsfall benötigt das Repository kein neues „Haupt“-Interface. Es braucht eine separate Methode neben den üblichen Optional/List/Page/Slice-Szenarien.

package com.example.shopdatajpa.catalog.repository;

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

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Lokales Scrolling-Szenario: die anderen üblichen Repository-Methoden bleiben daneben bestehen
    // Stabile Sortierung ist obligatorisch, um Duplikate/Lücken zwischen Fenstern zu vermeiden
    // ScrollPosition ist ein „Lesezeichen“, ab welcher Stelle das Lesen FORTGESETZT wird
    Window<Product> findFirst20ByStatusOrderByIdAsc(ProductStatus status,
                                                   ScrollPosition position);
}

Hier ersetzt Window nicht Page und Slice; es beantwortet eine andere Frage – wie man eine große Menge sequenziell, Fenster für Fenster, durchläuft.

Window im Service lesen und Position verschieben

Der Service muss genau zwei Dinge tun: das Lesen von Anfang an starten und dann wiederholen „Fenster gelesen → nächste Position berechnet → nächstes Fenster angefordert“, bis wir am Ende angelangt sind.

Ein kleiner, bewusst einfacher Ausschnitt (im echten Code würden Sie noch etwas mit den Produkten tun, aber für die Lehrklarheit reicht ein Zähler):

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;

// Start der Operation: „fangen von vorne an“ (und NICHT offset(0))
ScrollPosition position = ScrollPosition.offset();

int total = 0;

while (true) {
    // Lese das nächste Fenster (Portion) von der aktuellen Position
    Window<Product> window =
            productRepository.findFirst20ByStatusOrderByIdAsc(ProductStatus.ACTIVE, position);

    // Hier findet normalerweise die Verarbeitung der Fensterdaten statt (Export/Neuberechnung/usw.)
    total += window.size();

    // Wenn keine Daten mehr vorhanden sind — beenden
    if (window.isLast() || window.isEmpty()) break;

    // Nimm die Position des LETZTEN Elements des Fensters:
    // die nächste Abfrage setzt das Lesen NACH diesem Element fort (exklusive-Semantik)
    position = window.positionAt(window.size() - 1);
}

Beachten Sie die Zeile mit positionAt(window.size() - 1): Wir nehmen die Position des letzten Elements des aktuellen Fensters, damit die nächste Abfrage das Lesen nach diesem Element fortsetzt.

Wenn Sie in diesem Moment gedacht haben „aber warum nicht offset(20), offset(40)?“ – genau deshalb, weil Sie nicht manuell einen „Seitenzähler“ führen und verwirrt werden wollen, besonders wenn Sie später auf keyset-Scrolling umsteigen möchten.

Offset oder Keyset

In diesem Beispiel sind wir mit ScrollPosition.offset() gestartet. Das ist in Ordnung, um die Mechanik zu verstehen. Aber bedenken Sie, dass der offset-Ansatz für große Datenmengen teuer sein kann: Die Datenbank muss zuerst den benötigten Versatz erreichen und dann erst das Fenster ausliefern.

Keyset-Scrolling skaliert normalerweise besser, weil die Datenbank das Lesen „nach dem Schlüssel“ fortsetzt, nicht „überspringe N Zeilen“. Aus praktischer Sicht ist die Schlussfolgerung in unserem Übungsprojekt einfach: Wenn Sie über id scrollen, liegt der keyset-Modus hier sehr nahe, weil id eindeutig, non-null und gut indiziert ist.

Startposition

Lassen Sie uns noch einmal den Punkt festhalten, den Sie sich am besten mit Laser ins Gedächtnis brennen: Der Beginn der Operation ist ScrollPosition.offset(), nicht ScrollPosition.offset(0). Aufgrund der exklusiven Semantik überspringt offset(0) tatsächlich das erste Element.

5. Repository-Cleanup

Scrolling kann leicht überschätzt werden. Sobald Window aufgetaucht ist, möchte man es zur „modernsten“ Option erklären und das halbe Repository darum herum umschreiben. Das ist derselbe Fehler wie bei der native query als Standard: Ein nützliches Werkzeug wird plötzlich zu einem neuen Kult. Für normale Katalog-/Anwendungsfall-Lesungen bleiben Optional, List, Page und Slice die grundlegenden Verträge. Window lebt daneben, wenn man Fenster für Fenster durch ein großes Ergebnis gehen muss.

Eine nützliche kurze Checkliste hier ist:

Lesen vs. Referenz. Wenn die Felder der Entität benötigt werden – findById(). Wenn nur Entitäten per ID verknüpft werden müssen – getReferenceById(). Veraltete Namen aus alten Artikeln sollte man hier besser nicht verwenden.

Reserved methods leben um @Id herum. findById() und andere grundlegende Methoden arbeiten über die identifier property, nicht über ein beliebiges Feld, in dessen Namen Id vorkommt.

Der Rückgabetyp drückt den Anwendungsfall aus. Page – wenn total count benötigt wird, Slice – wenn nur „gibt es noch mehr“ wichtig ist, Window – wenn wir portioniert durch langes sequenzielles Lesen gehen.

Scrolling ist eine lokale Erweiterung, kein neuer Standard. Es ist praktisch, es als separate Methode für ein Szenario hinzuzufügen, nicht das gesamte ProductRepository um Window herum aufzubauen.

Dann hört Window auf, mit dem Rest der API zu streiten, und nimmt seinen normalen Platz ein: nicht anstelle der üblichen Verträge, sondern neben ihnen.

6. Typische Fehler beim Scrolling und Repository-Cleanup

Scrolling und Cleanup scheinen „Kosmetik“ zu sein, bis Sie Bugs auf dem Niveau „warum ist das erste Produkt verschwunden?“ oder „warum habe ich ein Repository mit 120 Methoden und habe Angst, es zu öffnen“ bekommen. In diesem Abschnitt sammeln wir die häufigsten Stolpersteine – keine Panik, nur damit Sie sie am Klang der herannahenden Schritte erkennen.

Fehler Nr. 1: mit ScrollPosition.offset(0) starten und das erste Element überspringen.
Das ist sehr unintuitiv, aber logisch: Scrolling ist exklusiv, die Position bedeutet „nach diesem“. Daher sind ScrollPosition.offset() und ScrollPosition.offset(0L) unterschiedliche Dinge. Ersteres bedeutet „Anfang“, zweiteres zeigt auf Element 0, und der nächste Schritt beginnt nach ihm.

Fehler Nr. 2: versuchen zu scrollen ohne stabile Reihenfolge.
Wenn die Methode kein OrderBy... hat oder die Sortierung „tanzen“ kann, riskieren Sie, Duplikate oder Lücken zwischen den Fenstern zu erhalten. Das ist kein „Bug von Spring Data“, sondern der Preis für das Fehlen einer totalen Ordnung. Der sicherste Anker ist id (eindeutig, nicht null). Für keyset-Scrolling wird die Anforderung non-null der sortierbaren Eigenschaften sogar kritisch.

Fehler Nr. 3: Window als „Page ohne Count“ wahrnehmen.
Window ist keine Seite mit Nummer. Es ist eine Datenportion mit Fortsetzungsposition. Wenn Sie anfangen, sich „page=7“ auszudenken, kehren Sie zum offset-Paradigma zurück, nur in einer verwirrteren Form. In diesem Moment ist es besser, ehrlich zu Page/Slice und Pageable zurückzukehren.

Fehler Nr. 4: veraltete getOne() / getById() weiterhin in den Code ziehen, „weil ich es in einem Artikel gesehen habe“.
In der modernen API von JpaRepository sind diese Methoden als veraltet markiert, und der richtige Weg ist getReferenceById(). Und dann entscheiden Sie bewusst, ob Sie eine Referenz oder einen tatsächlichen Ladevorgang über findById() benötigen.

Fehler Nr. 5: reserviertes findById() mit der Suche nach „einem beliebigen Feld, in dessen Namen Id vorkommt“ verwechseln.
Reserved methods arbeiten mit der identifier property (Feld @Id), unabhängig vom Eigenschaftsnamen, und Versuche, die Bedeutung zu „überschreiben“, führen zu Verwirrung. Wenn der Geschäftsschlüssel sku, code oder orderNumber ist, nennen Sie die Methoden auch so: findBySku, findByCode, findByOrderNumber.

1
Aufgabe
Spring Data JPA, Level 16, Lektion 4
Gesperrt
Erstes Fenster für Produkte mittels Window und ScrollPosition
Erstes Fenster für Produkte mittels Window und ScrollPosition
1
Aufgabe
Spring Data JPA, Level 16, Lektion 4
Gesperrt
Bereinigung einer langen derived-Methode durch kurzen JPQL
Bereinigung einer langen derived-Methode durch kurzen JPQL
1
Umfrage/Quiz
Spring Repositories, Level 16, Lektion 4
Nicht verfügbar
Spring Repositories
Optimierung von JPA-Abfragen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION