|
1 | 1 | # Schema Design für Softwareprojekte |
2 | 2 |
|
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. |
4 | 4 |
|
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. |
0 commit comments