Zum Inhalt springen

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.


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.

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 committen
sequenceDiagram
    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
MethodeZweck
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

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.

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?

ModusVerhalten
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.
LENIENTDas 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:

  1. pro Operation über WritePayload.createReadMode
  2. sonst über codamai.cdms.api.create-read-mode (Standard STRICT)
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"]

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 ist volatile im 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.

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:

  1. Schreiboperationen, die beide Datenbanken ändern, sind zu vermeiden. Eine fachliche Operation schreibt genau ein Ziel.
  2. Ist das unvermeidbar, wird die Reihenfolge bewusst gewählt: zuerst das Ziel, dessen Teilzustand harmlos ist.
  3. Der Restzustand muss erkennbar bleiben – über Auditing, Fehlercode oder einen Kompensationsschritt.
  4. markRollbackOnly() muss den gesamten Request abdecken.

Blob und Metadaten teilen sich keine Transaktion:

FehlerErgebnis
Verschieben der Datei scheitertException → Request rollt zurück → weder Datensatz noch Blob. Konsistent.
Verschieben klappt, Request rollt später zurückBlob bleibt, Datensatz fehlt. Verwaist.
Datensatz wird später gelöscht, Blob-Löschung scheitertDatensatz 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.