Transaktionen
Kurzantwort: Die Transaktionsgrenze liegt auf dem Request, nicht auf der
Service-Methode. CDMS benutzt weder einen Spring-PlatformTransactionManager
noch @Transactional für die Kernpfade, sondern führt pro EntityManager eine
ressourcenlokale Transaktion und klammert sie um den Request. Am Request-Ende
wird committet – es sei denn, irgendetwas hat markRollbackOnly() gesetzt.
Warum keine deklarativen Transaktionen?
Abschnitt betitelt „Warum keine deklarativen Transaktionen?“Das Persistenzziel (System- oder Mandanten-Datenbank) wird pro Entity-Typ
aufgelöst. Ein deklarativer Transaktions-Interceptor müsste sein Ziel schon vor
der ersten Entity-Operation kennen. Deshalb übernimmt
DatabaseRequestContext die Klammer selbst.
Lebenszyklus
Abschnitt betitelt „Lebenszyklus“erste Persistenzoperation eines Ziels im Thread → EntityManager erzeugen → Transaktion beginnen
weitere Operationen → vorhandener EntityManager → war die Transaktion beendet (z. B. nach Rollback), wird neu begonnen
Erfolgsfall am Request-Ende → alle offenen Transaktionen committen → alle EntityManager schließen
Fehlerfall → markRollbackOnly() → aktive Transaktionen dieses Threads sofort zurückrollen → Abschluss am Request-Ende rollt zurück statt zu committensequenceDiagram
participant A as API / System-Layer
participant RC as DatabaseRequestContext
participant EM as EntityManager
participant DB as Datenbank
A->>RC: getEntityManager(...)
RC->>EM: begin
A->>EM: persist / merge / query
EM->>DB: INSERT / UPDATE / SELECT
alt erfolgreich
A->>RC: commitThreadTransactions() oder Request-Ende
RC->>EM: commit
else fehlgeschlagen
A->>RC: markRollbackOnly()
RC->>EM: rollback
Note over RC: auch das Request-Ende committet danach nichts mehr
end
A->>RC: closeThreadEntityManagers() / Request-Ende
RC->>EM: close
Die API der Klammer
Abschnitt betitelt „Die API der Klammer“| Methode | Zweck |
|---|---|
getEntityManager(boolean) | liefert den EntityManager und stellt sicher, dass eine Transaktion aktiv ist |
markRollbackOnly() | markiert den gesamten Request als gescheitert; wirft selbst nie |
commitThreadTransactions() | definierter Commit-Punkt; wirft IllegalStateException, wenn schon rollback-only |
closeThreadEntityManagers() | Worker-Threads geben ihre Verbindungen zurück |
closeEntityManager() | @PreDestroy am Request-Ende: committen oder zurückrollen, dann schließen |
PersistenceLayerInterface.rollback() | rollt die aktive Transaktion des zuständigen EntityManagers zurück; mehrfach aufrufbar |
Der Fehlerpfad ist zentral verankert
Abschnitt betitelt „Der Fehlerpfad ist zentral verankert“CdmsExceptionMapper (@ControllerAdvice) ruft bei jeder
RuntimeException markRollbackOnly() auf – bevor die Antwort gebaut und der
Request-Scope geschlossen wird.
Folge: Eine Operation, die System- und Mandantendatenbank berührt hat, rollt am Request-Ende beide Ziele zurück, statt das nicht einzeln zurückgerollte Ziel zu committen.
Wichtig für neue Fehlerpfade: Im ressourcenlokalen Modell gibt es keine Spring-Rollback-Regeln nach Exception-Typ. Ob zurückgerollt wird, entscheidet ausschließlich der ausdrückliche Aufruf von
rollback()bzw.markRollbackOnly(). Ein vergessener Aufruf führt zum Commit.
CREATE + READ: STRICT und LENIENT
Abschnitt betitelt „CREATE + READ: STRICT und LENIENT“Nach einem Anlegen liest CDMS das Objekt zurück, um die Antwort gemäß Response Request zu füllen. Was passiert, wenn dieses Zurücklesen scheitert?
| Modus | Verhalten |
|---|---|
STRICT (Standard) | Create und Read laufen in derselben Transaktion. Scheitert der Read, wird zurückgerollt und die ursprüngliche Lese-Exception propagiert. Nichts bleibt übrig. |
LENIENT | Das Create wird zuerst dauerhaft committet, der Read läuft in einer frischen Transaktion. Scheitert er, bleibt das Create bestehen und es kommt eine CreateSucceededReadFailedException (HTTP 200, messageKey CDMS_CREATE_SUCCEEDED_READ_FAILED), die die id mitliefert. |
Auswahl:
- pro Operation über
WritePayload.createReadMode - sonst über
codamai.cdms.api.create-read-mode(StandardSTRICT)
flowchart TB
C["createObject"] --> V["Hooks · Validierung · persist · flush"]
V --> M{createReadMode}
M -->|STRICT| S["readObject in derselben Transaktion"]
S -->|Fehler| SR["Rollback – Create ist weg"]
M -->|LENIENT| L["commit"]
L --> LR["readObject in neuer Transaktion"]
LR -->|Fehler| LP["Teilerfolg 200 + id<br/>Create bleibt bestehen"]
Worker-Threads
Abschnitt betitelt „Worker-Threads“Verarbeitet ein Request Aufgaben in weiteren Threads (z. B. eine Hub-Abfrage über ein abstraktes Modell):
- jeder Thread bekommt eigene EntityManager mit eigenen Transaktionen
- der Request-Kontext muss ausdrücklich propagiert werden – sowohl die
Spring-Request-Attribute als auch der globale
RequestContextHolder - jeder Worker muss vor dem Ende
closeThreadContext()aufrufen, sonst bleiben Verbindungen bis zum Request-Ende blockiert markRollbackOnly()wirkt threadübergreifend (das Flag istvolatileim request-scoped Bean); der sofortige Rollback betrifft aber nur die EntityManager des aufrufenden Threads, der Rest folgt am Request-Ende
Sonderfall: Hat der aufrufende Thread bereits eine offene Schreibtransaktion, sind seine noch nicht committeten Zeilen für die Worker unsichtbar – ein Read per ID fände nichts. Deshalb liest der Hub in diesem Fall direkt im aufrufenden Thread, ohne Parallelität.
Atomarität über Datenbankgrenzen
Abschnitt betitelt „Atomarität über Datenbankgrenzen“Mit Datenbank-pro-Mandant kann eine Operation zwei Datenbanken berühren. Ohne verteilte Transaktion gibt es dafür keine Atomarität – auf JTA/XA wird bewusst verzichtet. Verbindliche Konsequenzen:
- Schreiboperationen, die beide Datenbanken ändern, sind zu vermeiden. Eine fachliche Operation schreibt genau ein Ziel.
- Ist das unvermeidbar, wird die Reihenfolge bewusst gewählt: zuerst das Ziel, dessen Teilzustand harmlos ist.
- Der Restzustand muss erkennbar bleiben – über Auditing, Fehlercode oder einen Kompensationsschritt.
markRollbackOnly()muss den gesamten Request abdecken.
Dateien und Transaktionen
Abschnitt betitelt „Dateien und Transaktionen“Blob und Metadaten teilen sich keine Transaktion:
| Fehler | Ergebnis |
|---|---|
| Verschieben der Datei scheitert | Exception → Request rollt zurück → weder Datensatz noch Blob. Konsistent. |
| Verschieben klappt, Request rollt später zurück | Blob bleibt, Datensatz fehlt. Verwaist. |
| Datensatz wird später gelöscht, Blob-Löschung scheitert | Datensatz weg, Blob bleibt. Verwaist. |
Die Löschung des Blobs läuft synchron im Request – also vor dem Commit. Ein Rollback danach stellt den Datensatz wieder her, den Inhalt nicht.