Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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.

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?

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, nutzt du den Checkpoint block-3-start. Das Checkpoint-System wird im separaten Abschnitt Checkpoints erklärt.

Warum Transaktionen in der Anwendung?

Transaktionen kennst du seit DB-1: Änderungen werden entweder dauerhaft gemacht oder verworfen. In DB-2 interessiert uns, wo diese Entscheidung im Anwendungscode liegt.

Betrachte einen einfachen Ticket-Workflow:

  1. Ein Ticket wird erstellt.

  2. Ein erster Kommentar wird gespeichert.

  3. Ein Event ticket_created wird in den Verlauf geschrieben.

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.

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.

@Transactional im Service-Layer

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 VMware, Inc. (2026).

Im Kursprojekt liegt die Grenze bewusst im Service:

@Transactional
TicketResponse createTicket(CreateTicketRequest request) {
    TicketEntity savedTicket = persistTicketWorkflow(request);
    return ticketMapper.toResponse(savedTicket);
}

Die Schichten haben dabei unterschiedliche Rollen:

SchichtRolle im Transaktionskontext
ControllerHTTP-Daten entgegennehmen, validieren und an den Service delegieren
Servicefachlichen Ablauf und Transaktionsgrenze festlegen
Repositoryeinzelne Datenbankoperationen kapseln
PostgreSQLConstraints, Fremdschlüssel und dauerhafte Speicherung erzwingen

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.

Commit und Rollback lesen

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 VMware, Inc. (2026).

Für den Unterricht reicht diese Faustregel:

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.

@Transactional
void falsch() {
    ticketRepository.save(ticket);
    try {
        eventRepository.save(event);
    } catch (RuntimeException ex) {
        // Problem: Methode laeuft danach normal weiter.
    }
}

Besser ist, den Fehler entweder bewusst weiterzugeben oder eine fachliche Alternative zu implementieren, die keinen halben Zustand hinterlässt.

Fachliche Invarianten

Eine Invariante ist eine Regel, die nach jedem erfolgreichen Vorgang gelten muss. Im Ticket-System können solche Regeln sein:

Manche Regeln gehören in PostgreSQL:

ALTER TABLE app_starter.tickets
    ADD CONSTRAINT tickets_status_check
    CHECK (status IN ('open', 'waiting', 'closed'));

Andere Regeln brauchen Service-Logik:

private void assertStatusTransition(String oldStatus, String newStatus) {
    if ("closed".equals(oldStatus)) {
        throw new ResponseStatusException(
            HttpStatus.BAD_REQUEST,
            "Geschlossene Tickets werden nicht wieder geoeffnet"
        );
    }
}

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.

Externe Seiteneffekte

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.

Riskant ist deshalb ein Ablauf wie:

Transaktion beginnt
    Ticket speichern
    E-Mail senden
    Event speichern schlaegt fehl
Rollback

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.

Für Block 3 genügt die Grundentscheidung:

Wir vertiefen hier keine Event-Architektur. Entscheidend ist, dass du die Grenze erkennst.

Optimistic Locking

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.

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 Hibernate Team (2026).

@Version
@Column(name = "version")
private Long version;

Die Datenbankmigration ergänzt die passende Spalte:

ALTER TABLE app_starter.tickets
    ADD COLUMN version BIGINT NOT NULL DEFAULT 0;

Die Idee ist bewusst einfach:

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.

Review-Fragen für Block 3

Beim Lesen eines Transaktionsablaufs helfen diese Fragen:

Eine gute Antwort nennt nicht nur @Transactional. Sie begründet, warum genau diese Grenze den fachlichen Zustand schützt.

References
  1. VMware, Inc. (2026). Using @Transactional. In Spring Framework Reference Documentation. https://docs.spring.io/spring-framework/reference/7.0/data-access/transaction/declarative/annotations.html
  2. Hibernate Team. (2026). Hibernate ORM User Guide. https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html