Skip to content

Commit 35648a9

Browse files
committed
chore: add block 4
1 parent 1c5ad31 commit 35648a9

4 files changed

Lines changed: 811 additions & 2 deletions

File tree

docs/6_schema_design/0_intro.md

Lines changed: 256 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,259 @@
11
# Schema Design für Softwareprojekte
22

3-
Dieser Block wird Datenbankschemata als Teil von Softwaredesign betrachten. Schwerpunkte sind technische IDs, fachliche Schlüssel, Constraints, Statusmodellierung, Audit-Felder, Soft Deletes und API-nahe Datenmodellierung.
3+
Nach Block 3 kann das Ticket-System fachliche Abläufe mit Transaktionen schützen. Ein Ticket, ein Kommentar und ein Event werden gemeinsam gespeichert oder gemeinsam zurückgerollt. Das löst aber noch nicht die Frage, welche Datenzustände überhaupt gültig sein dürfen.
44

5-
Die inhaltliche Ausarbeitung folgt später.
5+
Genau darum geht es in diesem Block. Ein Datenbankschema ist nicht nur eine technische Ablageform. Es ist ein Teil des Softwaredesigns: Es entscheidet, welche Pflichtfelder existieren, welche Werte erlaubt sind, welche Beziehungen gültig bleiben und welche Regeln auch dann gelten, wenn Daten nicht über den normalen Service geschrieben werden.
6+
7+
Für Block 4 startest du im abgeschlossenen Block-3-Zustand. Falls du mitten einsteigst, kannst du den Checkpoint `block-4-start` verwenden. Die Lösung mit den Schema-Design-Erweiterungen liegt im Checkpoint `block-4-complete`.
8+
9+
:::{important} Lernziele
10+
Nach diesem Block kannst du:
11+
12+
- technische IDs und fachliche Schlüssel unterscheiden.
13+
- `NOT NULL`, `UNIQUE`, `CHECK` und Foreign Keys als Schutzmechanismen begründen.
14+
- Varianten für Status- und Wertemodellierung vergleichen.
15+
- Audit-Felder und Soft Deletes kritisch einordnen.
16+
- Datenbankschema, Entity, DTO und API-Modell voneinander abgrenzen.
17+
- entscheiden, welche Regeln PostgreSQL und welche Regeln der Anwendungscode schützen sollen.
18+
:::
19+
20+
## Warum Schema Design Anwendungscode entlastet
21+
22+
Stell dir vor, ein Ticket wird mit leerem Titel, unbekanntem Status oder doppelter externer Referenz gespeichert. Der Anwendungscode kann solche Fälle prüfen. Das reicht aber nicht immer:
23+
24+
- Daten können über Tests, Skripte, Admin-Tools oder spätere Services geschrieben werden.
25+
- Validierung im Controller schützt nur den HTTP-Eingang.
26+
- Service-Logik schützt Abläufe, aber nicht automatisch jeden möglichen Schreibpfad.
27+
- Eine Datenbankregel bleibt auch dann aktiv, wenn sich der Anwendungscode verändert.
28+
29+
PostgreSQL kann viele einfache, dauerhafte Regeln direkt im Schema erzwingen, etwa Pflichtfelder, Eindeutigkeit, Wertelisten und Beziehungen {cite}`postgresql_documentation`. Der Anwendungscode wird dadurch nicht überflüssig. Er kann sich stärker auf Abläufe, Fehlermeldungen und fachliche Entscheidungen konzentrieren.
30+
31+
```{mermaid}
32+
flowchart LR
33+
API["API / DTO<br/>Was darf der Client senden?"]
34+
Service["Service<br/>Welcher Ablauf ist erlaubt?"]
35+
Entity["Entity<br/>Wie wird Java auf Tabellen abgebildet?"]
36+
Schema["PostgreSQL-Schema<br/>Welche Datenzustaende sind dauerhaft erlaubt?"]
37+
38+
API --> Service --> Entity --> Schema
39+
```
40+
41+
Die wichtigste Frage in einem Schema Review lautet deshalb:
42+
43+
> Welche fehlerhaften Daten dürfen gar nie dauerhaft gespeichert werden?
44+
45+
## Technische IDs und fachliche Schlüssel
46+
47+
Die Spalte `id` in `tickets` ist ein technischer Primärschlüssel. Sie ist stabil, kurz, gut für Foreign Keys geeignet und für JPA/Hibernate einfach zu verwenden. Für Menschen ist sie aber selten die wichtigste Referenz.
48+
49+
Eine fachliche Referenz hat eine andere Rolle. Sie kann aus einem Umsystem, einem Monitoring-Tool oder einer Supportnummer stammen. Im Block-4-Lösungsstand ergänzt die Migration eine optionale Spalte:
50+
51+
```sql
52+
ALTER TABLE app_starter.tickets
53+
ADD COLUMN external_reference TEXT;
54+
```
55+
56+
Diese Referenz ist fachlich bedeutsam, aber nicht zwingend für jedes Ticket vorhanden. Deshalb ist sie nullable. Wenn sie vorhanden ist, soll sie aber eindeutig sein:
57+
58+
```sql
59+
ALTER TABLE app_starter.tickets
60+
ADD CONSTRAINT tickets_external_reference_unique
61+
UNIQUE (external_reference);
62+
```
63+
64+
Das ist ein typischer Unterschied:
65+
66+
| Frage | Technische ID | Fachliche Referenz |
67+
| --- | --- | --- |
68+
| Wer nutzt sie primär? | Datenbank, ORM, Foreign Keys | Benutzerinnen, Umsysteme, Supportprozesse |
69+
| Muss sie sprechend sein? | Nein | Oft ja |
70+
| Darf sie sich ändern? | Möglichst nie | Je nach Fachprozess |
71+
| Braucht sie Eindeutigkeit? | Ja, als Primärschlüssel | Häufig, aber fachlich begründet |
72+
73+
Ein guter Entwurf verwendet technische IDs für robuste Beziehungen und fachliche Schlüssel für fachliche Wiedererkennung. Beides ist nicht dasselbe.
74+
75+
## Constraints als dauerhafte Schutzschicht
76+
77+
Constraints sind keine lästige Zusatzarbeit. Sie machen Regeln sichtbar und überprüfbar. Im Ticket-System gehören einige Regeln direkt in PostgreSQL:
78+
79+
```sql
80+
ALTER TABLE app_starter.tickets
81+
ADD CONSTRAINT tickets_priority_check
82+
CHECK (priority IN ('low', 'normal', 'high', 'urgent'));
83+
```
84+
85+
Diese Regel ist einfach, lokal und unabhängig vom konkreten HTTP-Request. Genau deshalb passt sie gut in die Datenbank.
86+
87+
| Regel | Typischer Schutz |
88+
| --- | --- |
89+
| Ticket braucht einen Titel | `NOT NULL` plus Anwendungvalidierung |
90+
| Status hat nur erlaubte Werte | `CHECK`, Enum oder Referenztabelle |
91+
| externe Referenz ist eindeutig | `UNIQUE` |
92+
| Kommentar gehört zu einem Ticket | Foreign Key |
93+
| Statuswechsel erzeugt Event | Service-Transaktion |
94+
| geschlossenes Ticket wird nicht wieder geöffnet | Service-Logik |
95+
96+
Das Schema und der Service arbeiten zusammen. PostgreSQL schützt den gültigen Datenzustand. Der Service schützt fachliche Abläufe, die mehrere Schritte, Bedingungen oder Fehlermeldungen brauchen.
97+
98+
## Status und Werte modellieren
99+
100+
Für Status, Priorität oder Event-Typen gibt es mehrere Modellierungsvarianten. Keine ist immer richtig.
101+
102+
| Variante | Geeignet, wenn ... | Risiko |
103+
| --- | --- | --- |
104+
| `CHECK` Constraint | die Werteliste klein und stabil ist | Schemaänderung nötig bei neuen Werten |
105+
| PostgreSQL-Enum | Werte sehr stabil und stark typisiert sind | spätere Änderungen können unbequemer werden |
106+
| Referenztabelle | Werte Metadaten, Sortierung oder Übersetzungen brauchen | mehr Tabellen und Joins |
107+
| Nur Java-Enum | Regel nur im Anwendungscode sichtbar sein soll | andere Schreibpfade können falsche Werte speichern |
108+
109+
Im Block-4-Lösungsstand ist `priority` bewusst als `TEXT` mit `CHECK` modelliert:
110+
111+
```sql
112+
ALTER TABLE app_starter.tickets
113+
ADD COLUMN priority TEXT;
114+
115+
UPDATE app_starter.tickets
116+
SET priority = 'normal'
117+
WHERE priority IS NULL;
118+
119+
ALTER TABLE app_starter.tickets
120+
ALTER COLUMN priority SET NOT NULL;
121+
122+
ALTER TABLE app_starter.tickets
123+
ADD CONSTRAINT tickets_priority_check
124+
CHECK (priority IN ('low', 'normal', 'high', 'urgent'));
125+
```
126+
127+
Diese mehrstufige Migration ist absichtlich. Bestehende Zeilen erhalten zuerst einen gültigen Wert. Erst danach wird `NOT NULL` aktiviert. Das passt zum Muster aus Schema Evolution: Produktionsdaten werden nicht ignoriert, sondern mitgedacht.
128+
129+
## Audit-Felder
130+
131+
Audit-Felder beantworten später einfache, aber wichtige Fragen:
132+
133+
- Wann wurde ein Ticket erstellt?
134+
- Wann wurde es zuletzt geändert?
135+
- Welches System oder welche Person hat es verändert?
136+
- Warum ist ein Ticket nicht mehr in normalen Listen sichtbar?
137+
138+
Im Starter gab es bereits `created_at`. Block 4 ergänzt `updated_at`:
139+
140+
```sql
141+
ALTER TABLE app_starter.tickets
142+
ADD COLUMN updated_at TIMESTAMPTZ;
143+
144+
UPDATE app_starter.tickets
145+
SET updated_at = created_at
146+
WHERE updated_at IS NULL;
147+
148+
ALTER TABLE app_starter.tickets
149+
ALTER COLUMN updated_at SET NOT NULL;
150+
```
151+
152+
In der Entity wird `updatedAt` bei Änderungen aktualisiert:
153+
154+
```java
155+
@PreUpdate
156+
void markUpdated() {
157+
updatedAt = OffsetDateTime.now();
158+
}
159+
```
160+
161+
Das ist für den Kurs bewusst einfach gehalten. In grösseren Systemen können Audit-Informationen auch durch Datenbank-Trigger, zentrale JPA-Auditing-Funktionen oder separate Event-Tabellen gepflegt werden. Entscheidend ist die Designfrage: Welche Nachvollziehbarkeit braucht das System später wirklich?
162+
163+
## Soft Deletes kritisch betrachten
164+
165+
Ein Soft Delete löscht eine Zeile nicht physisch. Stattdessen wird sie markiert, zum Beispiel mit `deleted_at`:
166+
167+
```sql
168+
ALTER TABLE app_starter.tickets
169+
ADD COLUMN deleted_at TIMESTAMPTZ;
170+
```
171+
172+
Der Block-4-Lösungsstand ergänzt einen einfachen API-Endpunkt:
173+
174+
```http
175+
DELETE /api/tickets/{id}
176+
```
177+
178+
Der Service setzt dabei `deleted_at`. Listenabfragen zeigen nur Tickets, bei denen `deleted_at IS NULL` gilt.
179+
180+
Das klingt praktisch, ist aber kein kostenloser Gewinn.
181+
182+
| Nutzen | Risiko |
183+
| --- | --- |
184+
| versehentliche Löschung kann eher nachvollzogen werden | jede Query muss gelöschte Zeilen korrekt ausfiltern |
185+
| Daten bleiben für Support oder Audit sichtbar | `UNIQUE` Regeln werden schwieriger, wenn gelöschte Werte wiederverwendet werden sollen |
186+
| Wiederherstellung ist möglich | Tabellen wachsen und werden fachlich schwerer zu lesen |
187+
188+
Soft Delete ist deshalb eine bewusste Entscheidung, nicht ein Standardreflex. Manchmal ist ein Statuswert besser. Manchmal braucht es Archivtabellen. Manchmal ist echte Löschung mit Audit-Event die klarere Lösung.
189+
190+
## Schema, Entity, DTO und API-Modell
191+
192+
Ein häufiger Fehler in Backend-Projekten ist, Schema, Entity und API-Antwort als dasselbe Modell zu behandeln. Das wirkt am Anfang bequem, erzeugt aber schnell enge Kopplung.
193+
194+
Im Block-4-Lösungsstand sieht man vier Sichten:
195+
196+
| Sicht | Aufgabe |
197+
| --- | --- |
198+
| Datenbankschema | dauerhafte Struktur, Constraints, Indizes und Datenregeln |
199+
| JPA-Entity | Mapping zwischen Java und Tabelle |
200+
| Request-DTO | was ein Client beim Erstellen senden darf |
201+
| Response-DTO | was die API nach aussen zurückgibt |
202+
203+
Die Entity kennt `deletedAt`, weil die Anwendung diese Spalte zum Ausblenden braucht. Die API-Antwort gibt `deletedAt` aber nicht aus. Das ist Absicht: Die API soll nicht jedes interne Implementierungsdetail nach aussen tragen.
204+
205+
```{mermaid}
206+
flowchart TB
207+
DB["tickets<br/>id, title, status, priority,<br/>external_reference, created_at,<br/>updated_at, deleted_at"]
208+
Entity["TicketEntity<br/>Mapping auf alle relevanten Spalten"]
209+
Request["CreateTicketRequest<br/>title, status, priority,<br/>externalReference, initialComment"]
210+
Response["TicketResponse<br/>id, title, status, priority,<br/>externalReference, version,<br/>createdAt, updatedAt"]
211+
212+
Request --> Entity
213+
Entity --> DB
214+
DB --> Entity
215+
Entity --> Response
216+
```
217+
218+
Diese Trennung hilft bei späteren Änderungen. Das Schema darf technische Spalten enthalten. Die Entity darf ORM-Details kennen. Die API bleibt ein bewusster Vertrag.
219+
220+
## Review-Fragen für Schema Design
221+
222+
Wenn du ein Schema in einem Softwareprojekt reviewst, helfen diese Fragen:
223+
224+
- Welche Daten dürfen nie `NULL` sein?
225+
- Welche Wertebereiche sind klein genug für einen `CHECK` Constraint?
226+
- Welche fachlichen Referenzen müssen eindeutig sein?
227+
- Welche Beziehungen brauchen Foreign Keys?
228+
- Welche Regel gehört in den Service, weil sie einen Ablauf beschreibt?
229+
- Welche Spalte ist intern und gehört nicht automatisch in die API?
230+
- Welche Queries müssen wegen Soft Deletes besonders sorgfältig formuliert werden?
231+
- Welche Migration ist für bestehende Daten sicher?
232+
233+
## Block-4-Checkpoint lesen
234+
235+
Für den Einstieg in Block 4:
236+
237+
```bash
238+
cd db-2-app
239+
./course-state create block-4-start ../work/db-2-app-block-4
240+
```
241+
242+
Für die Lösung:
243+
244+
```bash
245+
./course-state create block-4-complete ../work/db-2-app-block-4-loesung
246+
```
247+
248+
Im Lösungsstand sind besonders diese Dateien relevant:
249+
250+
- `src/main/resources/db/migration/V4__improve_ticket_schema_design.sql`
251+
- `src/main/java/ch/hftm/db2/ticketsystem/ticket/TicketEntity.java`
252+
- `src/main/java/ch/hftm/db2/ticketsystem/ticket/CreateTicketRequest.java`
253+
- `src/main/java/ch/hftm/db2/ticketsystem/ticket/TicketResponse.java`
254+
- `src/main/java/ch/hftm/db2/ticketsystem/ticket/TicketService.java`
255+
- `src/test/java/ch/hftm/db2/ticketsystem/ticket/TicketSchemaDesignTestcontainersIntegrationTest.java`
256+
257+
## Ausblick
258+
259+
Block 4 entscheidet, welche Datenzustände gültig sind. Block 5 fragt danach, wie diese Daten gezielt gelesen werden. Ein klares Schema macht Queries nicht automatisch perfekt, aber es macht sie verständlicher: Status, Priorität, fachliche Referenz und Soft-Delete-Markierung sind dann explizite Entscheidungen statt versteckte Annahmen im Code.

docs/checkpoints.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Ohne `--skip-slow` werden auch langsamere Testcontainers-Tests ausgeführt, sofe
5454
| `block-2-complete` | Lösung der V2-Migration für Ticket-Regeln |
5555
| `block-3-start` | Einstieg in Block 3 auf Basis des abgeschlossenen Block 2 |
5656
| `block-3-complete` | Lösung mit Transaktionsworkflow, Kommentaren, Events und Versionierung |
57+
| `block-4-start` | Einstieg in Block 4 auf Basis des abgeschlossenen Block 3 |
58+
| `block-4-complete` | Lösung mit fachlicher Referenz, Priorität, Audit-Feld und Soft-Delete-Markierung |
5759

5860
Die Liste wird erweitert, sobald weitere Blöcke vollständige App-Zustände brauchen.
5961

@@ -71,6 +73,13 @@ Wenn du die Lösung zu Block 3 anschauen möchtest, erzeugst du eine separate L
7173
./course-state create block-3-complete ../work/db-2-app-block-3-loesung
7274
```
7375

76+
Für Block 4 funktioniert dasselbe Muster:
77+
78+
```bash
79+
./course-state create block-4-start ../work/db-2-app-block-4
80+
./course-state create block-4-complete ../work/db-2-app-block-4-loesung
81+
```
82+
7483
Die Zielordner sind Beispiele. Entscheidend ist, dass der Zielordner ausserhalb von `db-2-app` liegt. Dadurch bleibt der Starter unverändert.
7584

7685
## Zustände prüfen

0 commit comments

Comments
 (0)