Skip to content

Commit f8bed5a

Browse files
committed
chore: add transactions
1 parent 9af11f5 commit f8bed5a

5 files changed

Lines changed: 743 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Warum ORM? -> Warum JPA/Hibernate? -> Warum Flyway-Migrationen? -> Wie arbeitet
2828

2929
Das Starterprojekt bleibt dabei bewusst unvollständig. Die Studierenden müssen zentrale Datenbanklogik wie Pflichtfelder, Constraints und Repository-Entscheide selbst entwickeln und begründen. Bereits gelaufene Flyway-Migrationen werden nicht nachträglich angepasst; Verbesserungen entstehen als neue Migrationen.
3030

31+
Ab Block 3 ergänzt `db-2-app` ein Checkpoint-System für Quereinstieg, Nacharbeit und Lösungseinsicht. Der normale Unterricht startet weiterhin beim Starter. Bei Bedarf erzeugt `./course-state create block-3-start ...` eine separate Arbeitskopie mit dem passenden Einstiegspunkt; `block-3-complete` enthält die sichtbare Lösung mit Transaktionsworkflow, Events und Optimistic Locking.
32+
3133
## Leitfallstudie
3234

3335
Alle Materialien arbeiten mit einem **Ticket-System für internen Support**. Die Fallstudie ist klein genug für Unterrichtslektionen und reich genug für typische Datenbankentwicklungsfragen:
Lines changed: 265 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,268 @@
11
# Transaktionen und Datenkonsistenz in Anwendungen
22

3-
Dieser Block wird Transaktionsgrenzen aus Anwendungssicht behandeln. Themen sind `@Transactional`, Commit, Rollback, fachliche Invarianten, externe Seiteneffekte und Optimistic Locking.
3+
Ein Ticket wird in einer Anwendung selten als einzelne Tabellenzeile behandelt. Wenn ein neues Ticket entsteht, braucht es oft zusätzlich einen ersten Kommentar, einen Ereigniseintrag und später einen Statuswechsel. Aus Sicht der Benutzerin ist das ein einziger fachlicher Vorgang. Aus Sicht der Datenbank sind es mehrere `INSERT`- und `UPDATE`-Anweisungen.
44

5-
Die inhaltliche Ausarbeitung folgt später.
5+
Genau an dieser Stelle werden Transaktionen im Anwendungscode wichtig. Eine Transaktion beantwortet nicht nur die Frage, ob PostgreSQL `COMMIT` oder `ROLLBACK` ausführt. Sie beantwortet vor allem die fachliche Frage: Welche Änderungen gehören so eng zusammen, dass sie nur gemeinsam dauerhaft gespeichert werden dürfen?
6+
7+
In Block 2 hast du `db-2-app` als Starterprojekt kennengelernt. Die Anwendung konnte Tickets lesen und erstellen. Für Block 3 bleibt der normale Lernpfad gleich: Du startest beim Starter und entwickelst weiter. Falls du mitten im Modul einsteigst oder eine Lösung anschauen musst, kannst du einen Checkpoint erzeugen:
8+
9+
```bash
10+
cd db-2-app
11+
./course-state create block-3-start ../work/db-2-app-block-3
12+
./course-state create block-3-complete ../work/db-2-app-block-3-loesung
13+
```
14+
15+
Der Checkpoint ist eine normale Arbeitskopie. Er ersetzt nicht den Unterrichtsweg, sondern macht definierte Zustände reproduzierbar.
16+
17+
:::{important} Lernziele
18+
Nach diesem Block kannst du:
19+
20+
- eine fachliche Transaktionsgrenze im Service-Layer begründen.
21+
- `@Transactional` als Werkzeug für deklarative Transaktionen in Spring einordnen.
22+
- erklären, wann eine Service-Methode committet oder zurückgerollt wird.
23+
- Ticket, Kommentar und Event als gemeinsamen Datenbankvorgang modellieren.
24+
- Datenbanktransaktionen von externen Seiteneffekten wie E-Mail oder HTTP Calls abgrenzen.
25+
- Optimistic Locking als Grundidee für gleichzeitige Bearbeitung erklären.
26+
- einen fehlerhaften Transaktionsablauf im Code Review erkennen.
27+
:::
28+
29+
## Warum Transaktionen in der Anwendung?
30+
31+
PostgreSQL kennt Transaktionen seit DB-1: Änderungen werden entweder dauerhaft gemacht oder verworfen. In DB-2 interessiert uns, wo diese Entscheidung im Anwendungscode liegt.
32+
33+
Betrachte einen einfachen Ticket-Workflow:
34+
35+
1. Ein Ticket wird erstellt.
36+
2. Ein erster Kommentar wird gespeichert.
37+
3. Ein Event `ticket_created` wird in den Verlauf geschrieben.
38+
39+
Wenn Schritt 2 oder 3 fehlschlägt, darf nicht einfach nur das Ticket übrig bleiben. Sonst zeigt die Anwendung ein Ticket ohne Verlauf oder ohne Startkommentar. Das ist technisch möglich, aber fachlich unvollständig.
40+
41+
```{mermaid}
42+
flowchart LR
43+
Request["POST /api/tickets"]
44+
Ticket["tickets<br/>neue Zeile"]
45+
Comment["ticket_comments<br/>optional erster Kommentar"]
46+
Event["ticket_events<br/>ticket_created"]
47+
Commit["COMMIT"]
48+
Rollback["ROLLBACK bei Fehler"]
49+
50+
Request --> Ticket --> Comment --> Event --> Commit
51+
Ticket -. Fehler .-> Rollback
52+
Comment -. Fehler .-> Rollback
53+
Event -. Fehler .-> Rollback
54+
```
55+
56+
Eine gute Transaktionsgrenze schützt den fachlichen Zusammenhang. Sie ist deshalb meistens nicht die Repository-Methode und nicht der Controller, sondern die Service-Methode, die den Ablauf koordiniert.
57+
58+
## `@Transactional` im Service-Layer
59+
60+
Spring unterstützt deklarative Transaktionen mit `@Transactional`. Die Annotation wird typischerweise durch einen Spring-Proxy ausgewertet: Beim Eintritt in die Methode wird eine Transaktion geöffnet oder wiederverwendet, am Ende wird bei normalem Rücksprung committet und bei passenden Exceptions zurückgerollt {cite}`spring_framework_transaction_reference`.
61+
62+
Im Kursprojekt liegt die Grenze bewusst im Service:
63+
64+
```java
65+
@Transactional
66+
TicketResponse createTicket(CreateTicketRequest request) {
67+
TicketEntity savedTicket = persistTicketWorkflow(request);
68+
return ticketMapper.toResponse(savedTicket);
69+
}
70+
```
71+
72+
Die Schichten haben dabei unterschiedliche Rollen:
73+
74+
| Schicht | Rolle im Transaktionskontext |
75+
| --- | --- |
76+
| Controller | HTTP-Daten entgegennehmen, validieren und an den Service delegieren |
77+
| Service | fachlichen Ablauf und Transaktionsgrenze festlegen |
78+
| Repository | einzelne Datenbankoperationen kapseln |
79+
| PostgreSQL | Constraints, Fremdschlüssel und dauerhafte Speicherung erzwingen |
80+
81+
Der Service ist der passende Ort, weil er weiss, welche Schritte fachlich zusammengehören. Das Repository weiss nur, wie eine Entity gespeichert wird. Der Controller sollte nicht entscheiden, welche Datenbankänderungen atomar sein müssen.
82+
83+
```{mermaid}
84+
sequenceDiagram
85+
autonumber
86+
participant C as TicketController
87+
participant S as TicketService
88+
participant T as tickets
89+
participant K as ticket_comments
90+
participant E as ticket_events
91+
92+
C->>S: createTicket(request)
93+
Note over S: @Transactional beginnt
94+
S->>T: Ticket speichern
95+
S->>K: Kommentar speichern
96+
S->>E: Event speichern
97+
Note over S: Methode endet ohne Fehler
98+
S-->>C: TicketResponse
99+
Note over S,E: COMMIT
100+
```
101+
102+
## Commit und Rollback lesen
103+
104+
In Spring gilt als Grundregel: Eine `RuntimeException` oder ein `Error` löst bei `@Transactional` standardmässig ein Rollback aus. Checked Exceptions werden nicht automatisch gleich behandelt, wenn nichts anderes konfiguriert ist {cite}`spring_framework_transaction_reference`.
105+
106+
Für den Unterricht reicht diese Faustregel:
107+
108+
- Wenn die Methode normal endet, wird committet.
109+
- Wenn eine unbehandelte Runtime Exception aus der Methode herausläuft, wird zurückgerollt.
110+
- Wenn ein Fehler abgefangen und verschluckt wird, sieht Spring am Methodenende keinen Rollback-Grund.
111+
112+
Das ist der Grund, warum Fehlerbehandlung nicht nur Stilfrage ist. Eine Service-Methode, die einen Fehler abfängt, loggt und trotzdem normal zurückkehrt, kann einen unvollständigen Vorgang committen.
113+
114+
```java
115+
@Transactional
116+
void falsch() {
117+
ticketRepository.save(ticket);
118+
try {
119+
eventRepository.save(event);
120+
} catch (RuntimeException ex) {
121+
// Problem: Methode laeuft danach normal weiter.
122+
}
123+
}
124+
```
125+
126+
Besser ist, den Fehler entweder bewusst weiterzugeben oder eine fachliche Alternative zu implementieren, die keinen halben Zustand hinterlässt.
127+
128+
## Fachliche Invarianten
129+
130+
Eine Invariante ist eine Regel, die nach jedem erfolgreichen Vorgang gelten muss. Im Ticket-System können solche Regeln sein:
131+
132+
- Ein neu erstelltes Ticket hat einen gültigen Status.
133+
- Ein Ticket mit Startkommentar hat einen Kommentar-Eintrag, der auf das Ticket zeigt.
134+
- Jeder Statuswechsel erzeugt ein Event.
135+
- Ein geschlossenes Ticket wird in diesem einfachen Modell nicht wieder geöffnet.
136+
137+
Manche Regeln gehören in PostgreSQL:
138+
139+
```sql
140+
ALTER TABLE app_starter.tickets
141+
ADD CONSTRAINT tickets_status_check
142+
CHECK (status IN ('open', 'waiting', 'closed'));
143+
```
144+
145+
Andere Regeln brauchen Service-Logik:
146+
147+
```java
148+
private void assertStatusTransition(String oldStatus, String newStatus) {
149+
if ("closed".equals(oldStatus)) {
150+
throw new ResponseStatusException(
151+
HttpStatus.BAD_REQUEST,
152+
"Geschlossene Tickets werden nicht wieder geoeffnet"
153+
);
154+
}
155+
}
156+
```
157+
158+
Beides ergänzt sich. PostgreSQL schützt zentrale Datenregeln unabhängig vom Schreibpfad. Der Service schützt fachliche Abläufe, die mehr Kontext brauchen als eine einzelne Spalte.
159+
160+
## Externe Seiteneffekte
161+
162+
Eine Datenbanktransaktion kann Datenbankänderungen zurückrollen. Sie kann aber keine bereits gesendete E-Mail zurückholen, keinen HTTP Call ungeschehen machen und keine Message aus einer externen Queue entfernen.
163+
164+
Riskant ist deshalb ein Ablauf wie:
165+
166+
```text
167+
Transaktion beginnt
168+
Ticket speichern
169+
E-Mail senden
170+
Event speichern schlaegt fehl
171+
Rollback
172+
```
173+
174+
Nach dem Rollback existiert das Ticket nicht, aber die E-Mail wurde schon verschickt. Das ist kein Fehler von PostgreSQL. Es ist eine falsch gewählte Grenze zwischen Datenbanktransaktion und Systemprozess.
175+
176+
Für Block 3 genügt die Grundentscheidung:
177+
178+
- Datenbankänderungen, die fachlich zusammengehören, liegen in einer kurzen Transaktion.
179+
- Externe Effekte werden bewusst danach ausgelöst oder in einem eigenen Muster behandelt.
180+
- Im Code Review wird geprüft, ob externe Effekte mitten in der Transaktion stehen.
181+
182+
Wir vertiefen hier keine Event-Architektur. Entscheidend ist, dass du die Grenze erkennst.
183+
184+
## Optimistic Locking
185+
186+
Nebenläufigkeit wird sichtbar, wenn zwei Personen dasselbe Ticket bearbeiten. Beide lesen den Status `open`. Person A setzt auf `waiting`, Person B setzt fast gleichzeitig auf `closed`. Ohne Schutz kann die spätere Speicherung die frühere Änderung überschreiben.
187+
188+
Optimistic Locking löst dieses Problem mit einer Versionsspalte. Die Entity erhält ein Feld mit `@Version`; Hibernate/JPA nutzt dieses Feld, um beim Speichern zu prüfen, ob der gelesene Stand noch aktuell ist {cite}`hibernate_orm_user_guide`.
189+
190+
```java
191+
@Version
192+
@Column(name = "version")
193+
private Long version;
194+
```
195+
196+
Die Datenbankmigration ergänzt die passende Spalte:
197+
198+
```sql
199+
ALTER TABLE app_starter.tickets
200+
ADD COLUMN version BIGINT NOT NULL DEFAULT 0;
201+
```
202+
203+
Die Idee ist bewusst einfach:
204+
205+
```{mermaid}
206+
sequenceDiagram
207+
autonumber
208+
participant A as Supporter A
209+
participant B as Supporter B
210+
participant DB as PostgreSQL
211+
212+
A->>DB: Ticket lesen, version = 0
213+
B->>DB: Ticket lesen, version = 0
214+
A->>DB: Status waiting speichern, version 0 -> 1
215+
B->>DB: Status closed mit version 0 speichern
216+
DB-->>B: Konflikt, Stand ist veraltet
217+
```
218+
219+
Optimistic Locking verhindert nicht, dass zwei Personen gleichzeitig arbeiten. Es verhindert, dass eine Anwendung unbemerkt auf veraltetem Stand speichert. Die fachliche Reaktion bleibt Aufgabe der Anwendung: neu laden, Konflikt melden oder Benutzerin entscheiden lassen.
220+
221+
## Checkpoint-System für diesen Block
222+
223+
Das Checkpoint-System ist bewusst nicht Git-basiert. Es verwendet normale Dateien in Overlays:
224+
225+
```text
226+
db-2-app/course/
227+
├── states.yml
228+
└── overlays/
229+
├── block-2-complete/
230+
└── block-3-complete/
231+
```
232+
233+
Die Zustände bauen aufeinander auf:
234+
235+
| Zustand | Verwendung |
236+
| --- | --- |
237+
| `block-2-start` | Normaler Starter für Block 2 |
238+
| `block-2-complete` | Lösung der V2-Migration |
239+
| `block-3-start` | Einstieg in Block 3 |
240+
| `block-3-complete` | Lösung mit Transaktionsworkflow |
241+
242+
Für den normalen Unterricht beginnst du im Starter. Für Quereinstieg oder Lösungseinsicht erzeugst du eine Kopie:
243+
244+
```bash
245+
./course-state create block-3-start ../work/db-2-app-block-3
246+
```
247+
248+
Dozierende können alle definierten Zustände prüfen:
249+
250+
```bash
251+
./course-state validate --skip-slow
252+
```
253+
254+
Ohne `--skip-slow` werden auch Testcontainers-Tests ausgeführt, sofern Docker oder Podman verfügbar ist.
255+
256+
## Review-Fragen für Block 3
257+
258+
Beim Lesen eines Transaktionsablaufs helfen diese Fragen:
259+
260+
- Welcher fachliche Vorgang wird geschützt?
261+
- Beginnt und endet die Transaktion an der richtigen Stelle?
262+
- Welche Tabellenänderungen müssen gemeinsam committen?
263+
- Welche Exception würde ein Rollback auslösen?
264+
- Werden Fehler versehentlich abgefangen?
265+
- Gibt es externe Seiteneffekte innerhalb der Transaktion?
266+
- Braucht der Vorgang Schutz gegen gleichzeitige Bearbeitung?
267+
268+
Eine gute Antwort nennt nicht nur `@Transactional`. Sie begründet, warum genau diese Grenze den fachlichen Zustand schützt.

docs/references.bib

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ @misc{spring_data_jpa_reference
3131
note = {Accessed on: 2026-04-23}
3232
}
3333

34+
@misc{spring_framework_transaction_reference,
35+
author = {{VMware, Inc.}},
36+
title = {Using @Transactional},
37+
journal = {Spring Framework Reference Documentation},
38+
year = {2026},
39+
url = {https://docs.spring.io/spring-framework/reference/7.0/data-access/transaction/declarative/annotations.html},
40+
note = {Accessed on: 2026-06-04}
41+
}
42+
3443
@misc{hibernate_orm_user_guide,
3544
author = {{Hibernate Team}},
3645
title = {Hibernate ORM User Guide},

0 commit comments

Comments
 (0)