CodeGym /Kurse /Spring REST & MVC /Suche nach q und Bereich dueAfter/ dueBefore

Suche nach q und Bereich dueAfter/ dueBefore

Spring REST & MVC
Level 23, Lektion 1
Verfügbar

1. Semantik von q und dueAfter/dueBefore

Es ist sehr leicht, in die Falle zu tappen: „Wir fügen q hinzu – dann wird gesucht, wir fügen dueAfter hinzu – dann gibt es einen Datumsfilter.“ Aber solche Parameter haben eine unangenehme Eigenschaft: Wenn Sie die Regeln nicht festgelegt haben, dann entwerfen Sie keine API mehr – Sie führen ein Experiment mit den Kunden durch. Eine Person erwartet eine Suche „über alle Felder“, eine andere nur über den Titel, eine dritte denkt, dass dueAfter das angegebene Datum nicht einschließt, eine vierte ist vom Gegenteil überzeugt. Und dann versuchen Sie noch, Tests zu schreiben … und stellen plötzlich fest, dass die Tests keine Code-Überprüfung sind, sondern eine Überprüfung von Vermutungen.

Die grundlegenden Equality-Filter haben uns bereits das einfachste Szenario geliefert: ein Feld, ein Wert, eine Vergleichsregel. q und der Datumsbereich sind komplexer, gerade weil hier ein Teilstring, Grenzen und Kombinationen von Parametern ins Spiel kommen. Daher ist es besser, sie separat zu besprechen, anstatt sie mit den Filtern für exakte Übereinstimmung zu vermischen.

Das Ziel dieser Vorlesung ist es daher, q, dueAfter und dueBefore in vertragliche Parameter zu verwandeln. Ein Vertrag bedeutet in unserem Kontext nicht „ungefähr so“, sondern klare Vereinbarungen: nach welchen Feldern wird gesucht, wie werden Zeichenketten verglichen, welches Datumsformat wird erwartet, sind die Bereichsgrenzen inklusiv oder exklusiv, und was ist mit ungültigen Kombinationen zu tun? In dieser Form wird der Endpunkt vorhersagbar: Dieselbe Anfrage auf denselben Daten liefert dasselbe Ergebnis – und niemand beschwert sich (na ja, fast).

Vertrag für q

Der Parameter q steht für „query“, einfach ausgedrückt: eine Zeichenkette, nach der wir Aufgaben suchen möchten. In der Praxis ist q kein Equality-Filter. Wir vergleichen das Feld nicht mit einem genauen Wert, wie bei status=IN_PROGRESS. Wir suchen, ob ein Teilstring in den vorher festgelegten Textfeldern vorkommt. Und genau dieses „vorher festgelegt“ ist der Schlüsselteil. Ohne diese Festlegung wird q zu einem kleinen schwarzen Loch: Die Kunden werden erwarten, dass die Suche über Tags, Bearbeiter, Status, UUIDs und alles gleichzeitig funktioniert. Und unser Lehrdienst ist weder Google noch Yandex (und Gott sei Dank für die Latenz).

Für die Task Tracker API wählen wir bewusst eine einfache Semantik: q sucht als Teilstring (substring match), ohne Rangfolge, ohne „intelligente“ Wortformen, ohne Fuzzy-Matching. Wir suchen in title und description. Warum nicht auch in tags und assigneeName? Weil wir dafür bereits separate Filter tag und assigneeName haben – sie sind genauer, einfacher für den Kunden und enthalten weniger „Magie“. Die Textsuche ist eine bequeme „Suchzeichenkette“ für den Fall, dass man sich nicht genau an den Parameternamen erinnert.

Damit q den Vertrag nicht bricht, ist es hilfreich, sofort einige kleine, aber wichtige Regeln festzulegen:

Was Regel in unserer API Warum wichtig
q Suche nach title und description Der Kunde kennt vorab die Suchoberfläche
Groß-/Kleinschreibung case-insensitive Der Benutzer muss sich nicht merken, ob „Report“ oder „report“
Leerzeichen q.trim() „ report “ sollte sich nicht wie eine separate Anfrage verhalten
Leerer Wert null/blank → Filter nicht angewendet Sonst wird q= zu einer seltsamen „Suche nach Leere“
„Intelligenz“ Keine Rangfolge oder Relevanz Dies ist eine Lern-API und ein In-Memory-Speicher

Eine separate Kleinigkeit: q ist ein Query-Parameter, daher muss die URL URL-codiert sein. Der String annual report sollte als q=annual%20report (oder q=annual+report, das kommt auch häufig vor) ankommen. Wenn Sie das nicht klarstellen, werden einige Anfragen „aus unerfindlichen Gründen nicht funktionieren“.

Vertrag für dueAfter und dueBefore

Der Datumsbereich ist die nächste Kategorie von Parametern, die man nicht „auf gut Glück“ einführen sollte. Die Wörter after und before klingen zwar offensichtlich, aber in der Praxis beginnen die Leute sofort zu diskutieren: Bedeutet dueBefore=2026-03-01 „bis zum ersten März einschließlich“ oder „streng genommen früher“? Und wenn eine Aufgabe überhaupt kein dueDate hat, ist sie dann geeignet oder nicht? Und was, wenn der Kunde beide Grenzen angibt, sie aber vertauscht hat? Und schon befinden wir uns nicht mehr in der Programmierung, sondern in der Rekonstruktion archäologischer Bedeutungen.

In diesem Kurs wählen wir eine möglichst verständliche, „alltägliche“ Semantik. Beide Parameter beziehen sich nur auf ein einziges Feld der Aufgabe – dueDate. Wir verwenden LocalDate, arbeiten also mit einem Datum „Tag-Monat-Jahr“ ohne Uhrzeit und Zeitzone. Und wir wählen inklusive Grenzen: dueAfter bedeutet „nicht vor dem angegebenen Datum“, und dueBefore bedeutet „nicht nach dem angegebenen Datum“. Das heißt:

dueAfter → dueDate >= dueAfter
dueBefore → dueDate <= dueBefore

Das ist sehr benutzerfreundlich: Wenn jemand „nach dem 1. März“ schreibt, erwartet er normalerweise, dass der 1. März auch dazugehört. Man könnte auch eine exklusive Semantik wählen, aber dann müssten Sie dies explizit kommunizieren und später erklären, warum es „an der Grenze nicht funktioniert“. Wir wählen das, was am wenigsten überrascht.

Tabelle zur Verdeutlichung:

Parameter Angewendet auf Bedeutung Beispiel
dueAfter Task.dueDate Fälligkeit nicht vor dem Datum dueAfter=2026-03-01
dueBefore Task.dueDate Fälligkeit nicht nach dem Datum dueBefore=2026-03-31

Eine weitere wichtige vertragliche Entscheidung: Was tun mit Aufgaben ohne dueDate? In der Realität kommen solche Aufgaben ständig vor: „irgendwann erledige ich es“. Für unsere API ist es logisch, Folgendes zu vereinbaren: Wenn der Datumsbereich nicht angegeben ist, stören Aufgaben ohne dueDate nicht und werden in das Ergebnis aufgenommen. Wenn der Kunde jedoch dueAfter und/oder dueBefore angibt, dann werden Aufgaben ohne dueDate nicht als passend betrachtet, weil Sie buchstäblich nach „Aufgaben mit einer Fälligkeit“ fragen, diese aber nicht vorhanden ist. Dies ist nicht die einzig mögliche Wahl, aber sie ist die vorhersagbarste.

Um es visuell darzustellen, kann man den Bereich als „Korridor“ betrachten:

flowchart LR
    A[dueAfter] -->|einschließlich| R[Zulässige dueDate]
    R -->|einschließlich| B[dueBefore]

3. Binden von Query-Parametern in Spring MVC

Wenn wir JSON über @RequestBody empfangen, kommt Jackson zum Einsatz: Er liest den Request-Body, parst das JSON und erstellt ein DTO. Query-Parameter funktionieren jedoch anders: Sie werden von Spring MVC über den Konvertierungsmechanismus für einfache Typen (ConversionService) gebunden. Aus diesem Grund haben Anfänger oft die seltsame Idee: „Aber Jackson kann doch Daten verarbeiten, soll er das auch hier machen.“ Das tut er nicht. Hier ist ein anderer Weg – und das ist normal.

Wenn Sie einen klaren Vertrag wünschen, können Sie zwei Dinge tun. Erstens: Wählen Sie den Parametertyp als LocalDate, damit die Semantik „Datum ohne Uhrzeit“ ist. Zweitens: Weisen Sie Spring explizit darauf hin, dass Sie das ISO-Datumsformat erwarten. In den meisten Projekten mit LocalDate wird ohnehin ISO (YYYY-MM-DD) verwendet, aber explizit ist besser als implizit, besonders in einem Lehrprojekt.

Minibeispiel einer Controller-Signatur (noch ohne Kriterienobjekt, um die Mechanik live zu sehen):

import java.time.LocalDate;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public Object getTasks(
        // Vertrag: q ist ein optionaler Parameter; ein leerer/blanker Wert wird als fehlender Filter betrachtet
        @RequestParam(required = false) String q,

        // Vertrag: ISO-8601 Datum (YYYY-MM-DD), ohne Uhrzeit und Zeitzone
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dueAfter,

        // Vertrag: ISO-8601 Datum (YYYY-MM-DD), ohne Uhrzeit und Zeitzone
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dueBefore
) {
    return null; // hier wird der Service aufgerufen
}

Wichtig zu beachten: Wenn der Kunde dueAfter=2026-03-01 sendet, wandelt Spring dies ordnungsgemäß in LocalDate.of(2026, 3, 1) um. Sendet er dagegen dueAfter=hello, tritt der Fehler vor Ihrem Service auf, weil die Typkonvertierung fehlschlägt. Und das ist gut: Solche Eingaben sollten nicht in die Anwendungslogik gelangen.

In unserem Projekt gibt es bereits eine globale Fehlerbehandlung und ProblemDetail, daher sollten solche Fehler nicht in einer „HTML-Seite mit Serverfehler“ enden, sondern in einem sauberen 400 Bad Request mit einem verständlichen code. Wir schreiben das hier nicht neu (das haben wir bereits im Modul über Fehler gemacht), aber es ist wichtig, den Grund zu verstehen: Das Datum ist bereits auf der Stufe des Bindens der Query-Parameter „kaputtgegangen“.

4. Ungültiger Datumsbereich: 400 Bad Request

Es gibt einen schmalen Grat zwischen „nichts gefunden“ und „Sie haben eine sinnlose Anfrage gesendet“. Wenn der Kunde dueAfter=2026-04-01&dueBefore=2026-03-01 anfordert, hat er keine „schlechte Suche“ durchgeführt. Er hat die Bereichsgrenzen vertauscht und eine logisch unmögliche Bedingung geschaffen. Aus Sicht des API-Vertrags handelt es sich um strukturell ungültige Eingabe, also um einen 400 Bad Request. Wenn Sie eine leere Liste zurückgeben, verstecken Sie das Problem: Der Kunde könnte wochenlang denken, dass „es einfach keine Aufgaben gibt“, obwohl er in Wirklichkeit eine Anfrage stellt, die niemals ein Ergebnis liefern kann.

Auf Codeebene kann dies eine sehr kleine Prüfung sein. Selbst wenn die Parameter zunächst einzeln ankommen, ist es besser, den Bereich vor der eigentlichen Filterung zu validieren, anstatt zu warten, bis er „zufällig“ ein leeres Ergebnis liefert. Zum Beispiel:

import java.time.LocalDate;

private void validateDueRange(LocalDate dueAfter, LocalDate dueBefore) {
    // Vertrag: Wenn beide Grenzen angegeben sind, müssen sie einen gültigen Bereich bilden (dueAfter <= dueBefore)
    if (dueAfter != null && dueBefore != null && dueAfter.isAfter(dueBefore)) {
        // Vertrag: Dies ist kein "nichts gefunden", sondern eine ungültige Anfrage → 400 Bad Request
        throw new InvalidInputException("dueAfter must be before or equal to dueBefore");
    }
}

Hier ist InvalidInputException Ihre anwendungsspezifische Ausnahme, die bereits in der Lage ist, in eine ProblemDetail mit 400 umgewandelt zu werden. Wichtig ist nicht der genaue Klassenname, sondern die Bedeutung: Wir signalisieren dem Kunden „Eingabe ungültig“, nicht „nichts gefunden“.

Wenn Sie sich dabei ertappen zu denken: „Ach, es ist doch nur ein Filter, warum 400?“, denken Sie daran, dass der HTTP-Vertrag ebenfalls Teil der Benutzererfahrung ist. 400 ermöglicht es dem Kunden, schnell zu verstehen: Die Anfrage muss repariert werden, nicht „warten, bis Daten erscheinen“.

5. Prädikate matchesText und matchesDueDate

Damit der Filtercode nicht zu einer einzigen riesigen Spaghetti wird, ist es praktisch, jeden Filter als kleine Regel zu halten: „Passt die Aufgabe zu dieser Bedingung“. Das haben wir bereits für die Basisfilter (status, priority, archived, assigneeName, tag) gemacht. Jetzt fügen wir zwei weitere Prädikate hinzu: matchesText und matchesDueDate. Sie sollten so einfach wie möglich, lesbar und vor allem dem soeben festgelegten Vertrag entsprechen.

Beginnen wir mit der Textsuche. Ein wichtiges Detail: Wenn Sie den Vergleich ohne Berücksichtigung der Groß-/Kleinschreibung mit toLowerCase durchführen, verwenden Sie besser Locale.ROOT, um nicht von der Systemsprache abhängig zu sein (das klassische „türkische i“ ist kein Witz, sondern ein echtes Problem). Und noch etwas: description kann null sein, daher sollten Sie sich keine NPE einhandeln.

import java.util.Locale;

private boolean matchesText(Task task, String q) {
    // Vertrag: Wenn q nicht angegeben oder blank ist, wird der Textfilter nicht angewendet
    if (q == null || q.isBlank()) return true;

    // Vertrag: Normalisiere die Anfrage (trim + case-insensitive)
    String needle = q.trim().toLowerCase(Locale.ROOT);

    // Suche nur in den vorher festgelegten Feldern: title und description
    String title = task.getTitle().toLowerCase(Locale.ROOT);

    // description kann null sein – laut Vertrag ist das einfach "leerer Text", kein Grund zum Absturz
    String description = nullToEmpty(task.getDescription()).toLowerCase(Locale.ROOT);

    return title.contains(needle) || description.contains(needle);
}

nullToEmpty – ein kleiner Helfer (ja, er ist banal; aber Sie fangen dann keine Ausnahmen „ohne Grund“):

private String nullToEmpty(String value) {
    // Hilfsmethode für null-sichere Arbeit mit Zeichenketten in Filtern
    return value == null ? "" : value;
}

Jetzt der Datumsbereich. Wir haben vereinbart, dass der Filter auf dueDate angewendet wird, die Grenzen inklusiv sind und Aufgaben ohne dueDate nicht passen, wenn mindestens eine Grenze angegeben ist. Daraus ergibt sich eine kompakte Logik:

import java.time.LocalDate;

private boolean matchesDueDate(Task task, LocalDate dueAfter, LocalDate dueBefore) {
    // Vertrag: Wenn der Bereich nicht angegeben ist, wird der dueDate-Filter nicht angewendet
    if (dueAfter == null && dueBefore == null) return true;

    LocalDate dueDate = task.getDueDate();

    // Vertrag: Wenn der Kunde eine Filterung nach Fälligkeit wünscht, sind Aufgaben ohne dueDate nicht geeignet
    if (dueDate == null) return false;

    // Vertrag: dueAfter inklusiv → dueDate >= dueAfter
    if (dueAfter != null && dueDate.isBefore(dueAfter)) return false;

    // Vertrag: dueBefore inklusiv → dueDate <= dueBefore
    return dueBefore == null || !dueDate.isAfter(dueBefore);
}

Und jetzt das Schönste: Die Komposition liest sich fast wie ein menschlicher Satz. Innerhalb Ihrer allgemeinen Prüfung fügen Sie einfach zwei neue Bedingungen hinzu: „und der Text passt, und das Datum passt“. Zum Beispiel in einem Stil, der dem ähnelt, den wir bereits in der ersten Vorlesung hatten:

private boolean matchesAllFilters(Task task, BasicFilters basic, String q,
                                 LocalDate dueAfter, LocalDate dueBefore) {
    // Setze die UND-Prädikate zusammen – jedes Teil ist für seinen Parameter und seinen Vertrag zuständig
    return matchesBasicFilters(task, basic)
            && matchesText(task, q)
            && matchesDueDate(task, dueAfter, dueBefore);
}

Hier ist BasicFilters ein hypothetisches Objekt / Satz von Parametern aus der vorherigen Vorlesung (oder einfach ein Satz von Argumenten). Wichtig ist, dass q und der Datumsbereich zu zwei weiteren Filtern werden, nicht zu einem separaten „Suchsubsystem“.

6. Beispiele für Anfragen und Antworten

Wenn Sie eine API entwerfen, ist es sehr nützlich, sich regelmäßig in die Rolle des Kunden zu versetzen. Nicht im philosophischen Sinne von „Ich fühle den Schmerz des Kunden“, sondern in einem sehr praktischen: „Ich sehe eine URL und verstehe, was sie tut“. Wenn die Anfrage wie ein Zauberspruch aus einem Buch über schwarze Magie aussieht, haben wir irgendwo die Semantik nicht klar genug besprochen.

Beispiel einer einfachen Textsuche (beachten Sie die Leerzeichen – sie müssen kodiert werden):

GET http://localhost:8080/api/v1/tasks?q=annual%20report
Accept: application/json

Beispiel einer Suche nach Datumsbereich:

GET http://localhost:8080/api/v1/tasks?dueAfter=2026-03-01&dueBefore=2026-03-31
Accept: application/json

Beispiel eines kombinierten Szenarios (wir gehen noch nicht im Detail auf die Reihenfolge „Filterung + Sortierung + Paginierung“ ein – das kommt in der nächsten Vorlesung, aber der Kunde kann es bereits so machen):

GET http://localhost:8080/api/v1/tasks?q=report&dueAfter=2026-03-01&page=0&size=20&sort=updatedAt,desc
Accept: application/json

Wenn alles korrekt ist, bleibt die Antwort in derselben Form PagedResponse<T>. Beispiel (gekürzt, um die Struktur zu zeigen, nicht im JSON zu ertrinken):

{
  "items": [
    { "id": "c2f0...", "title": "Annual report draft", "status": "IN_PROGRESS" }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 1,
  "totalPages": 1,
  "sort": "updatedAt,desc"
}

Und nun die wichtigen negativen Szenarien.

Wenn der Kunde ein falsches Datumsformat sendet, z. B. dueAfter=2026-99-99, dann kann Spring dies nicht in LocalDate konvertieren, und Ihr Fehlerbehandler sollte 400 zurückgeben. Beispiel für problem details (Felder können im Detail variieren, aber der Sinn ist derselbe):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Failed to convert value of 'dueAfter' to LocalDate",
  "instance": "/api/v1/tasks",
  "code": "INVALID_INPUT"
}

Wenn der Kunde die Bereichsgrenzen vertauscht hat und Sie die Strategie „das ist ein 400“ gewählt haben, erhalten Sie eine ähnliche Antwort, nur dass die detail-Angabe sich auf Ihren Vertrag bezieht, nicht auf die Konvertierung:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "dueAfter must be before or equal to dueBefore",
  "instance": "/api/v1/tasks",
  "code": "INVALID_INPUT"
}

Beachten Sie, wie viel nützlicher dies ist, als stillschweigend eine leere Liste zurückzugeben. Eine leere Liste sagt: „Es gibt keine Daten“. Eine solche ProblemDetail sagt: „Repariere die Anfrage“.

7. Typische Fehler bei q und Datumsbereich

In diesem Thema scheitert es meistens nicht am Code, sondern an den Vereinbarungen. Das heißt, die Anwendung „funktioniert irgendwie“, aber dann stellt sich plötzlich heraus, dass verschiedene Personen ein unterschiedliches Verständnis davon haben, was q und was „after/before“ bedeutet. Die gute Nachricht ist: Fast alle typischen Fehler lassen sich mit einem einzigen Mittel heilen – explizite Semantik und kleine, lesbare Prüfungen im Code.

Fehler Nr. 1: Zu glauben, dass q eine „intelligente Suche nach allem, was sich bewegt, ist“.
Wenn Sie die Suchfelder nicht benennen, wird der Kunde erwarten, dass q auch nach Tags, Bearbeiter, Status und noch etwas anderem sucht. In der Folge wirkt die API „instabil“: mal findet sie etwas, mal nicht. Die Lösung ist einfach: Legen Sie fest, dass q nach title und description sucht (und halten Sie sich daran), und verwenden Sie für die anderen Felder separate Filter.

Fehler Nr. 2: q nicht zu normalisieren und „magische“ Unterschiede zwischen q=report und q= report zu erhalten.
Ohne trim() und die Behandlung von isBlank() kann es leicht passieren, dass derselbe Text, eingegeben mit Leerzeichen, keine Ergebnisse liefert. Der Benutzer fühlt sich, als würde er mit einem Computer aus den 90ern diskutieren. Die Normalisierung von q ist keine „unnötige Verarbeitung“, sondern Teil des Vertrags: Ein leerer String gilt als fehlender Filter.

Fehler Nr. 3: Einen Vergleich ohne Berücksichtigung der Groß-/Kleinschreibung mit toLowerCase() ohne Locale.ROOT durchzuführen.
Auf Ihrem Rechner kann eine andere Spracheinstellung aktiv sein als auf dem Server, und plötzlich verhält sich der Zeichenkettenvergleich seltsam. Dies ist ein seltener, aber sehr unangenehmer Fehler: Er lebt leise vor sich hin und taucht dann im ungünstigsten Moment auf. Verwenden Sie toLowerCase(Locale.ROOT) – und schlafen Sie ruhiger.

Fehler Nr. 4: Sich nicht zu entscheiden, was mit Aufgaben ohne dueDate zu tun ist.
Wenn der Kunde dueAfter oder dueBefore angibt und Sie Aufgaben mit dueDate = null in das Ergebnis aufnehmen, wird ein Teil der Leute überrascht sein: „Ich habe nach Fälligkeit gefragt“. Wenn Sie sie ausschließen, wird ein anderer Teil sagen: „Warum sind meine unbefristeten Aufgaben verschwunden?“. Es ist wichtig, nicht die „einzig richtige“ Lösung zu erraten, sondern eine Regel zu wählen und zu fixieren. In unserem Vertrag: Wenn ein Bereich angegeben ist, sind Aufgaben ohne dueDate nicht geeignet.

Fehler Nr. 5: dueAfter > dueBefore als „nun ja, eben leeres Ergebnis“ zu interpretieren.
Damit verstecken Sie den Fehler des Kunden, und er kann sehr lange nicht verstehen, warum seine Anfrage nichts findet. Für Bereichsparameter ist es logischer, dies als ungültige Eingabe zu betrachten und 400 Bad Request mit einer verständlichen Ursache zurückzugeben.

1
Aufgabe
Spring REST & MVC, Level 23, Lektion 1
Gesperrt
Suche nach `q` in den Feldern `title` und `description`
Suche nach `q` in den Feldern `title` und `description`
1
Aufgabe
Spring REST & MVC, Level 23, Lektion 1
Gesperrt
Bereich `dueAfter` und `dueBefore` für das Feld `dueDate`
Bereich `dueAfter` und `dueBefore` für das Feld `dueDate`
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION