Eine API-Anforderung klingt oft einfach: «Zeige offene Tickets», «Liefere eine kompakte Liste» oder «Wie viele Tickets hat jede Priorität?». Trotzdem ist der Datenzugriff eine Designentscheidung. Eine sehr kurze Repository Method kann eine klare Anforderung gut ausdrücken. Bei mehreren Bedingungen wird derselbe Methodenname schnell schwer lesbar. Und ein Report mit Zählungen ist in SQL häufig klarer als eine verschachtelte Objektabfrage.
In diesem Block lernst du nicht möglichst viele Abfragesprachen. Du lernst, für eine konkrete Anforderung die kleinste verständliche Query-Form zu wählen. Das Ticket-System bleibt dabei der gemeinsame Fall: aktive Tickets listen, nur die nötigen Felder laden und eine Auswertung für den Support erstellen.
Falls du nach Block 4 in den Kurs einsteigst, erzeugst du zuerst den Checkpoint block-5-start. Für die Kontrolle steht block-5-complete bereit. Die Befehle und die Idee dahinter findest du im Abschnitt Checkpoints.
Die API-Anforderung bestimmt die Query¶
Ein Repository ist keine Sammlung möglichst cleverer Queries. Es ist die Übersetzung einer fachlichen Leseanforderung in einen gezielten Datenzugriff. Vor jeder technischen Entscheidung helfen vier Fragen:
Was braucht der aufrufende Endpunkt wirklich? Eine Liste braucht häufig weniger Felder als eine Detailansicht.
Wie komplex ist die fachliche Bedingung? Ein einzelner Statusfilter ist etwas anderes als eine gruppierte Auswertung mit Kommentaren.
Muss die Query über Java-Entities oder direkt über das Schema sprechen? Beide Perspektiven sind sinnvoll, aber nicht für denselben Zweck.
Welche Daten dürfen nicht erscheinen? Im Ticket-System gehören soft-gelöschte Tickets nicht in normale Listen und Reports.
Die Entscheidung ist kein Wettbewerb. Gute Query Design bedeutet nicht, immer SQL oder immer JPQL zu verwenden. Es bedeutet, dass ein Team die gewählte Form später noch lesen, testen und bei einer Anforderungsänderung anpassen kann.
| Kriterium | Frage im Review |
|---|---|
| Lesbarkeit | Versteht eine neue Entwicklerin die fachliche Absicht ohne die ganze Query zu entschlüsseln? |
| Wartbarkeit | Ist die Query an einer passenden Stelle und mit einem stabilen Vertrag gekapselt? |
| Schema-Nähe | Braucht die Anforderung Tabellen, PostgreSQL-Funktionen oder ein exakt sichtbares SQL-Statement? |
| Geladene Daten | Werden nur Felder und Zeilen geladen, die der Endpunkt tatsächlich verwendet? |
| Datenwachstum | Gibt es eine Annahme, die bei vielen Tickets kritisch werden könnte? |
Der letzte Punkt ist eine Erwartung, noch keine Optimierung. Pagination, Indexe, Query-Pläne und ORM-Probleme vertiefst du in Block 6.
Repository Methods für einfache Filter¶
Spring Data kann aus einem konventionellen Methodennamen eine parametrisierte Query ableiten. Das ist für einfache, sprechende Filter nützlich VMware, Inc. (2026). Im Block-4-Lösungsstand verwendet das Repository bereits diese Methode:
List<TicketEntity> findByStatusAndDeletedAtIsNullOrderByCreatedAtDesc(String status);Sie beantwortet eine konkrete Anforderung: «Zeige aktive Tickets eines Status, neueste zuerst.» Der Name enthält Filter, Soft-Delete-Regel und Sortierung. Das ist noch gut lesbar, weil jede Komponente eine offensichtliche Bedeutung hat.
@GetMapping
List<TicketResponse> findTickets(@RequestParam(required = false) String status) {
return ticketService.findTickets(status);
}Der Endpunkt GET /api/tickets?status=open verwendet diese Repository Method. Ohne Parameter ruft der Service die noch einfachere Methode findByDeletedAtIsNullOrderByCreatedAtDesc() auf.
Wann der Name zu viel versteckt¶
Eine hypothetische Methodensignatur wie diese ist ein Warnsignal:
findByStatusAndPriorityAndCreatedAtAfterAndDeletedAtIsNullOrderByPriorityDescCreatedAtDesc(...)Sie ist nicht falsch. Aber der Methodenname wird zur eigenen kleinen Abfragesprache. Bei Änderungen muss ein Team die Grammatik des Namens statt eine klar formatierte Query lesen. Spätestens bei Projektionen, optionalen Kriterien, Joins oder Aggregationen ist JPQL beziehungsweise native SQL häufig die bessere Erklärung.
JPQL für fachliche Listen¶
JPQL spricht über Entities und deren Java-Felder, nicht direkt über Tabellen und Spalten. Im Ticket-System heisst die Entity TicketEntity, ihr Feld für den Soft Delete deletedAt und ihr Feld für den Erstellzeitpunkt createdAt. JPQL nutzt genau diese Namen VMware, Inc. (2026).
Die Ticketübersicht benötigt keine externe Referenz, keine Version und keinen Änderungszeitpunkt. Deshalb ist eine ganze TicketEntity als Rückgabewert unnötig breit. Der Block-5-Lösungsstand führt dafür ein eigenes Antwortmodell ein:
@Value
public class TicketListItemResponse {
Long id;
String title;
String status;
String priority;
OffsetDateTime createdAt;
}Die Repository Query erzeugt dieses DTO direkt mit einer Constructor-Projektion:
@Query("""
SELECT new ch.hftm.db2.ticketsystem.ticket.TicketListItemResponse(
t.id, t.title, t.status, t.priority, t.createdAt
)
FROM TicketEntity t
WHERE t.deletedAt IS NULL
AND (:status IS NULL OR t.status = :status)
ORDER BY t.createdAt DESC
""")
List<TicketListItemResponse> findActiveTicketOverview(@Param("status") String status);Wichtig sind drei Aspekte:
TicketEntityundt.createdAtsind Java-Namen. Die Tabelleapp_starter.ticketsund die Spaltecreated_aterscheinen hier absichtlich nicht.new ...TicketListItemResponse(...)legt fest, welche fünf Werte das DTO erhält. Die Reihenfolge muss zum Konstruktor passen.t.deletedAt IS NULList kein Detail, das der Endpunkt weglassen darf. Es ist Teil der fachlichen Bedeutung von aktive Tickets.
Der Service normalisiert einen vorhandenen Status und übergibt sonst null. Damit kann derselbe Endpunkt alle oder nur offene, wartende oder geschlossene Tickets anzeigen:
@GetMapping("/overview")
List<TicketListItemResponse> findTicketOverview(
@RequestParam(required = false) String status
) {
return ticketService.findTicketOverview(status);
}Die resultierende API ist bewusst schlank:
GET /api/tickets/overview?status=openEine Projektion ist keine pauschale Performance-Garantie. Sie ist zuerst ein klarer Vertrag: Diese Liste braucht genau diese Felder. Ob und wann das zudem relevant Datenmenge spart, prüfst du im nächsten Block mit konkreten Zugriffsmustern.
Native SQL für Reporting¶
Ein Support-Team möchte wissen, wie viele aktive Tickets jede Kombination aus Status und Priorität hat und wie viele Kommentare zu diesen Tickets gehören. Diese Anforderung verbindet zwei Tabellen und zwei unterschiedliche Zählungen. Genau hier kann native SQL die klarste Form sein.
SELECT
t.status AS status,
t.priority AS priority,
COUNT(DISTINCT t.id) AS "ticketCount",
COUNT(c.id) AS "commentCount"
FROM app_starter.tickets t
LEFT JOIN app_starter.ticket_comments c ON c.ticket_id = t.id
WHERE t.deleted_at IS NULL
GROUP BY t.status, t.priority
ORDER BY t.status, t.priority;Diese Query spricht absichtlich die PostgreSQL-Tabelle und ihre Spaltennamen an. Sie liefert keine Entity, sondern eine Zeile pro Status-Prioritäts-Gruppe.
| Teil | Bedeutung |
|---|---|
LEFT JOIN | Tickets ohne Kommentar bleiben im Report sichtbar. |
COUNT(DISTINCT t.id) | Ein Ticket mit zwei Kommentaren wird trotzdem nur einmal als Ticket gezählt. |
COUNT(c.id) | Es zählt nur vorhandene Kommentarzeilen; ein Ticket ohne Kommentar erhöht diese Zahl nicht. |
GROUP BY t.status, t.priority | Jede Kombination aus Status und Priorität wird zu einer Report-Zeile. |
WHERE t.deleted_at IS NULL | Ausgeblendete Tickets verfälschen den aktiven Support-Report nicht. |
Die Aliase bilden einen Vertrag zwischen SQL und einer internen Interface-Projektion:
interface TicketStatusPriorityReportProjection {
String getStatus();
String getPriority();
long getTicketCount();
long getCommentCount();
}Das Repository führt die Query als nativeQuery = true aus. Der Service übersetzt die interne Projektion anschliessend explizit in TicketStatusPriorityReportResponse. So bleibt die API nicht an die technische Projektion des Repositories gekoppelt.
@GetMapping("/reports/status-priority")
List<TicketStatusPriorityReportResponse> findStatusPriorityReport() {
return ticketService.findStatusPriorityReport();
}Native SQL ist besonders passend, wenn die SQL-Struktur selbst wichtig ist: Reporting, datenbankspezifische Funktionen oder eine Abfrage, die über Tabellen und Spalten klarer verständlich wird. Sie koppelt den Code aber stärker an das konkrete Schema. Eine Umbenennung von deleted_at ist hier zum Beispiel nicht durch das Entity-Mapping verborgen.
Soft Deletes gehören in die Anforderung¶
Block 4 hat festgelegt, dass DELETE /api/tickets/{id} keine Zeile entfernt. Der Service setzt deleted_at. Damit hat jede normale Lesefunktion eine zusätzliche Pflichtfrage:
Soll dieser Use Case nur aktive Tickets sehen oder ausdrücklich auch ausgeblendete?
Für die normale Ticketliste, die Übersicht und den Status-/Prioritätsreport lautet die Antwort: nur aktive Tickets. Deshalb erscheint der Filter in jeder der drei Query-Formen:
| Query-Form | Schutz vor gelöschten Tickets |
|---|---|
| Repository Method | AndDeletedAtIsNull im Namen |
| JPQL | WHERE t.deletedAt IS NULL |
| native SQL | WHERE t.deleted_at IS NULL |
Ein häufiger Fehler ist, nur den Service zu betrachten. Wenn ein neues Repository eine direkte Query ergänzt, kann es den bisher sicheren Listenpfad umgehen. Soft Delete ist daher ein gutes Beispiel, warum Query Reviews nicht nur nach Syntax fragen, sondern nach fachlichen Filtern.
Query Review: klein, klar und überprüfbar¶
Nutze diese Checkliste, bevor du eine neue Query akzeptierst:
Welche API-Anforderung beantwortet sie in einem Satz?
Ist eine Repository Method noch kürzer und klarer?
Wenn JPQL verwendet wird: Stimmen Entity- und Feldnamen, Rückgabetyp und DTO-Konstruktor überein?
Wenn native SQL verwendet wird: Sind Tabellen, Joins, Aliasnamen und Datenbankspezifika bewusst gewählt?
Lädt die Query nur benötigte Spalten und Zeilen?
Sind Soft Deletes, Mandantenfilter oder andere fachliche Sichtbarkeitsregeln enthalten?
Bei Aggregationen: Werden Join-Zeilen versehentlich mehrfach gezählt?
Kann ein Test mit realem PostgreSQL den wichtigsten Datenfall beweisen?
Typische Fehlentscheide¶
| Beobachtung | Bessere Frage |
|---|---|
| «SQL ist immer schneller.» | Welche Daten braucht dieser Use Case, und wie messen wir bei Bedarf die tatsächliche Ausführung? |
| «Eine Entity kann ich immer zurückgeben.» | Welche Felder soll diese API wirklich versprechen? |
| «Ein langer Methodenname spart eine Query.» | Macht der Name die Bedingung für das Team wirklich verständlicher? |
«COUNT(t.id) reicht.» | Vervielfacht der Join Tickets durch Kommentare oder Labels? |
| «Der Soft-Delete-Filter ist bereits im Service.» | Kann diese neue Repository Query den Service-Pfad umgehen? |
Transfer: vom Query Design zur Performance¶
Nach diesem Block kannst du die Query-Form begründen. Das ist die Voraussetzung für die nächste Frage: Wie verhält sie sich mit vielen Daten und Beziehungen? In Block 6 untersuchst du N+1-Probleme, zu breite Entity-Ladungen, Pagination, EXPLAIN und Indexe.
Der wichtigste Merksatz bleibt:
Wähle die kleinste Query-Form, die die fachliche Anforderung klar, vollständig und testbar ausdrückt.
- VMware, Inc. (2026). Spring Data JPA Reference Documentation. https://docs.spring.io/spring-data/jpa/reference/