CodeGym /Kurse /Hibernate deep-dive /flush: die Synchronisierungsphase

flush: die Synchronisierungsphase

Hibernate deep-dive
Level 4, Lektion 0
Verfügbar

1. Symptom: SQL erscheint vor Ende der Transaktion

Wenn du dir schon das SQL-Log angeschaut hast (auf diesem Kurs ist das fast so wie den Wetterbericht vor dem Rausgehen zu checken), dann hast du sicher schon dieses merkwürdige Gefühl gehabt: du bist noch mitten in der Service-Methode, der Code ist noch nicht fertig – und im Log ist schon ein UPDATE oder INSERT aufgetaucht. Und im Kopf entsteht sofort die Hausmanns-Theorie: „Hibernate hat wohl selbst committed." Spoiler: nein.

Wir werfen gerne drei verschiedene Ereignisse in einen Topf: die Änderung des Objekts im Speicher, das Absenden von SQL an die Datenbank und den Abschluss der Transaktion. Im „manuellen SQL" kommen diese Dinge meist nacheinander (man schickt ein UPDATE – es läuft raus – man committed), weshalb das Gehirn die Verbindung „SQL = Finale" lernt. In Hibernate können diese Ereignisse zeitlich auseinandergezogen sein. Und das Wort, das Dirty Checking mit SQL verbindet, ist flush.

Wir haben bereits gesehen, dass Hibernate eine Änderung an einer managed-Entität schon vor jedem SQL bemerken kann. Jetzt brauchen wir das nächste Puzzleteil: In welchem Moment wird diese bemerkte Änderung überhaupt zu einer Datenbankabfrage – und warum ist das eine eigene Phase und kein anderer Name für commit?

Drei Welten: Objekt im Speicher, Persistence Context und Datenbank

Damit flush aufhört, wie Magie auszusehen, lohnt es sich, etwas Ordnung in die „Geographie" der Anwendung zu bringen. Stell dir vor, wir haben drei verschiedene „Schauplätze", und dasselbe Feld product.name kann gleichzeitig in drei Zuständen existieren. Klingt beunruhigend, aber für ein ORM ist das normaler Alltag – dafür ist es schließlich da.

Die erste Welt ist ein gewöhnliches Java-Objekt im Speicher. Du rufst product.setName("Phone Pro") auf und änderst das Feld im Objekt. Das passiert sofort: ohne Netzwerk, ohne JDBC und ohne Datenbank. Die zweite Welt ist der Persistence Context (der Kontext, in dem Hibernate Managed-Entitäten, Snapshots und eine Änderungswarteschlange hält). Die dritte Welt ist die echte Datenbank (PostgreSQL in unserem Commerce Persistence Lab), in der der Wert in einer Tabellenzeile liegt.

Es hilft, das so zu sehen (einfaches Schema):

flowchart LR
    A["Java-Objekt (Product) Felder werden über Setter geändert"] --> B["Persistence Context managed entity + Snapshots + Aktionswarteschlange"]
    B --> C["Datenbank (PostgreSQL) Tabellenzeilen"]

Der Kernpunkt ist dieser: Wenn du ein managed-Objekt änderst, geschehen die Änderungen sofort in Welt A (Objekt), und Hibernate beginnt zu verstehen, dass der Snapshot abweicht (Welt B) – aber das muss Welt C (Datenbank) noch nicht sofort berühren. Der Übergang B → C wird eben flush genannt.

3. Was flush ist – ohne Mystik

Im umgangssprachlichen Englisch klingt flush ja nach Toilettenspülung – daher möchte man manchmal denken: „flush gedrückt – alles weggespült für immer." In Hibernate ist die Bedeutung eine andere und viel technischer. flush ist die Synchronisierung des Zustands des Persistence Context mit der Datenbank: Hibernate nimmt die aufgelaufenen Änderungen der managed-Entitäten, verwandelt sie in SQL-Befehle (INSERT/UPDATE/DELETE) und schickt sie im Rahmen der laufenden Transaktion an die Datenbank.

Wichtig: flush „sucht nicht aus der Luft nach Änderungen". Er stützt sich auf Dirty Checking: Hibernate stellt fest, was sich geändert hat, und bereitet sich darauf vor, das bei der Synchronisierung in die Datenbank zu schreiben. Die Reihenfolge ist: Wir ändern das Objekt → Hibernate bemerkt es (Dirty Checking) → beim flush wird daraus echtes SQL.

Man kann sich das als sehr kurzen internen Algorithmus vorstellen (kein exakter Hibernate-Bytecode, aber das richtige Modell):

// Modell: Was grob innerhalb von Hibernate bei der Synchronisierung passiert
1) Im Persistence Context gibt es managed-Objekte.
2) Für jedes managed-Objekt gibt es einen Snapshot.
3) Vor flush vergleicht Hibernate Snapshot und aktuellen Zustand.
4) Gibt es Unterschiede – wird SQL gebildet.
5) SQL wird an die Datenbank geschickt (aber die Transaktion wird nicht geschlossen).

Daraus ist bereits ersichtlich, warum flush nicht commit ist: flush schickt SQL ab, commit schließt die Transaktion. SQL kann innerhalb einer Transaktion mehrfach abgeschickt werden, aber „den Deal abschließen" kann man nur einmal.

4. flush und commit: verschiedene Ebenen der Endgültigkeit

Die Verwechslung von flush und commit ist vor allem deshalb teuer, weil sie dazu führt, das SQL-Log falsch zu lesen. Wenn du ein UPDATE im Log siehst, hat man intuitiv das Gefühl: „Fertig, der Datensatz ist definitiv gespeichert." Aber in der Transaktionswelt tritt dieses „definitiv" bei commit ein, nicht bei flush.

Lass uns das sorgfältig vergleichen:

Eigenschaft flush commit
Was passiert Synchronisiert Änderungen aus dem Persistence Context in die DB, sendet SQL Schließt die Transaktion in der DB ab (fixiert Änderungen)
Kann mehrmals in einer Transaktion vorkommen Ja Nein (logisch einmal pro Transaktion)
Garantiert, dass Änderungen „für immer bleiben" Nein (Rollback ist möglich) Ja (bei erfolgreichem Commit)
Kann einen Constraint-/Uniqueness-Fehler vor Methodenende auslösen Ja Ja, aber häufig siehst du das Problem später, wenn kein flush war
Zusammenhang mit Dirty Checking Direkt: flush nutzt die Ergebnisse von Dirty Checking Indirekt: commit löst üblicherweise flush aus (oder führt dazu)

Wenn du ein „menschlicheres" Bild willst: flush ist wie Strg+S drücken (Entwurf auf Festplatte speichern), und commit ist wie Veröffentlichen klicken (Artikel veröffentlichen) oder „Zahlung abschließen". Du kannst die Datei gespeichert haben und später trotzdem Änderungen widerrufen, den Branch zurücksetzen oder sie niemals dem Nutzer schicken. Die Rolle des „Widerrufs" in einer Transaktion übernimmt rollback.

Und noch ein wichtiger Aspekt: flush findet innerhalb einer Transaktion statt. Das bedeutet: SQL wird in der Datenbank ausgeführt, aber andere Transaktionen müssen diese Änderungen üblicherweise nicht sehen, solange commit noch aussteht (wir gehen nicht in Isolation und Sichtbarkeitsebenen – wir halten nur die Grundidee fest).

5. Beispiele für flush: drei Mini-Szenen

Beispiel 1: Setter ist nicht gleich SQL

Der Kerngedanke hier: Ein Setter ist kein SQL. Selbst wenn du die managed-Entität geändert hast, muss Hibernate nicht in derselben Millisekunde ein UPDATE schreiben. Es kann bis zum Synchronisierungspunkt warten. Das ist keine „Faulheit", sondern die normale Architektur des Unit of Work.

Stellen wir uns vor, wir arbeiten im Commerce Persistence Lab und ändern den Produktnamen. Das Beispiel ist bewusst klein gehalten – um den Kern zu sehen und nicht in Schichten zu versinken.

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;

// Managed-Entität laden: sie kommt in den Persistence Context (Welt B)
Product product = entityManager.find(Product.class, 1L);

// Feld nur im Speicher ändern (Welt A)
product.setName("Phone Pro");

// In diesem Moment ist Product im Speicher bereits geändert,
// Hibernate hat die Änderung bemerkt (Dirty Checking),
// aber UPDATE muss noch nicht an die DB (Welt C) geschickt worden sein.

Der Ablauf ist folgender: find() gibt ein managed-Objekt zurück. Du änderst das Feld, Dirty Checking merkt sich, dass das Objekt „schmutzig" ist – d. h. vom Snapshot abweicht. Aber SQL zu schicken ist noch zu früh: Hibernate hat den Synchronisierungspunkt noch nicht erreicht.

Und hier braucht es Disziplin: Wenn man nur den Java-Code betrachtet, fängt man leicht an zu fragen: „Warum erscheint UPDATE im Log manchmal und manchmal nicht?" Die Antwort ist immer dieselbe: Schau auf den Zeitpunkt des flush, nicht auf den Zeitpunkt des Setter-Aufrufs.

Beispiel 2: Explizites flush() als Synchronisierungspunkt

Jetzt fügen wir genau eine Zeile hinzu – entityManager.flush(). Das ist wie ein Flag im Code zu setzen: „Hier möchte ich, dass alles Aufgelaufene zu echtem SQL wird und in die Datenbank geht."

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;

Product product = entityManager.find(Product.class, 1L);
product.setName("Phone Pro");

// Hibernate explizit bitten, aufgelaufene Änderungen mit der DB zu synchronisieren.
// Wichtig: Das ist KEIN Commit – die Transaktion bleibt offen.
entityManager.flush(); // hier ist Hibernate verpflichtet, SQL zu senden

Wenn du den SQL-Trace aktiviert hast (unser aktivierbares Profil), siehst du im Log etwas wie:

-- Beispiel-SQL, das direkt nach flush() erscheinen kann
update product set name=? where id=?

Nochmal: flush() bedeutet kein Commit. Es bedeutet: „Schick SQL jetzt sofort ab, aber schließ die Transaktion nicht." Als Beobachtungs- und Diagnosewerkzeug ist das sehr praktisch: Du kannst gezielt einen Synchronisierungspunkt setzen und das Systemverhalten noch vor Methodenende beobachten.

Beispiel 3: flush hat SQL gesendet, dann kam rollback

Das tückischste Szenario für Einsteiger sieht so aus: Du rufst flush() auf, siehst SQL im Log, freust dich – und dann kommt am Methodenende eine Exception, die Transaktion wird zurückgerollt, und in der Datenbank hat sich „plötzlich nichts geändert". Es wirkt, als ob die Datenbank SQL ignoriert. Tatsächlich hat die Datenbank das SQL innerhalb der Transaktion brav ausgeführt und dann die Transaktion brav zurückgerollt.

Schauen wir uns ein vereinfachtes Service-Szenario an (im Stil labsupport, um das nicht mit der „echten" Business-Logik zu vermischen). Wir werfen nach dem flush absichtlich eine Exception.

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameProductAndFail(EntityManager entityManager) {
    // Transaktionsstart auf Spring-Ebene (Grenzen sind wichtig fürs Verständnis von Rollback)
    Product product = entityManager.find(Product.class, 1L);

    // Managed-Entität ändern: die Änderung lebt noch im Persistence Context
    product.setName("Draft name");

    // Erzwungene Synchronisierung: SQL geht in der OFFENEN Transaktion an die DB
    entityManager.flush();                  // SQL abgeschickt

    // Exception führt zu Rollback: die Änderungen werden nicht „für immer"
    throw new RuntimeException("rollback"); // aber commit findet nicht statt
}

Was du aus diesem Beispiel als „Mantra" mitnehmen solltest:

flush kann zu echtem SQL führen, aber bis zum Commit ist alles noch umkehrbar.

Ja, es gibt Feinheiten wie Sequences (sequence) in PostgreSQL, die ihren Wert auch bei Rollback erhöhen können – aber das ist eine eigene Geschichte über ID-Generierung. Im Kontext dieser Vorlesung reicht die Grundregel: Daten, die durch INSERT/UPDATE/DELETE innerhalb einer Transaktion geändert wurden, gelten erst als „für immer", wenn die Transaktion committed wurde.

6. flush im Unit of Work: kurze Zeitachse

Wenn du in diesem Kurs das SQL-Log liest, hilft es sehr, im Kopf nicht „Java-Zeilen", sondern „Phasen des Unit of Work" zu halten. In Hibernate passiert fast alles Interessante genau an den Phasengrenzen. flush ist eine solche Grenze: Sie trennt „Änderungen im Speicher aufgelaufen" von „Änderungen an die Datenbank gesendet".

Eine praktische Zeitachse sieht so aus:

sequenceDiagram
    participant Code as Java-Code (Service)
    participant PC as Persistence Context
    participant DB as PostgreSQL

    Code->>PC: "find() -> managed entity"
    Code->>PC: "setName(\"Phone Pro\")"
    Note over PC: Dirty Checking bemerkt die Änderung
    Code->>PC: "flush() (explizit oder automatisch)"
    PC->>DB: "SQL senden (UPDATE/INSERT/DELETE)"
    Note over DB: SQL ausgeführt, aber Transaktion noch offen
    Code->>DB: "commit (am Ende der Transaktion)"

Dieses Bild ist nicht der Schönheit halber da, sondern für den täglichen Einsatz: Du wirst SQL „vor Methodenende" sehen, und deine Aufgabe ist es, nicht in Panik zu verfallen, sondern dich zu fragen: „Wo ist hier der flush?" Wenn flush da war – alles logisch. Wenn du ihn nicht explizit aufgerufen hast, bedeutet das, ein automatischer Synchronisierungstrigger hat ausgelöst – und genau der erzeugt meist das Gefühl von „SQL hat von selbst geschossen".

7. Typische Fehler beim ersten Kennenlernen von flush

Bevor wir weitergehen, lohnt es sich, typische Fehler festzuhalten. Sie sind tückisch, weil von außen alles „funktionierend" aussieht, aber das Modell im Kopf falsch zusammengesetzt wird. Ein falsches Modell im Persistence Layer endet fast immer damit, dass man Symptome kuriert statt Ursachen.

Fehler Nr. 1: Denken, dass flush und commit dasselbe sind.
Wenn du das denkst, wird jedes SQL in der Mitte einer Methode wie ein „frühzeitiges Commit" wirken – und du wirst Spring, Hibernate, die Mondphase und den Nachbarn verdächtigen. Das richtige Modell ist einfacher: flush sendet SQL im Rahmen einer offenen Transaktion, commit schließt die Transaktion und macht Änderungen endgültig.

Fehler Nr. 2: SQL im Moment des Setter-Aufrufs erwarten.
Einsteiger lesen Code manchmal als „setName() aufgerufen – also kommt jetzt gleich UPDATE". Aber ein Setter ändert das Java-Objekt. Hibernate kann bis zum Synchronisierungspunkt warten. Das Ergebnis: Man schaut ins Log, sieht kein UPDATE, fügt save() hinzu, dann noch ein save(), und wundert sich dann, warum es mehr Anfragen gibt, aber weniger Klarheit.

Fehler Nr. 3: Dirty Checking ignorieren und denken, flush „erledigt alles selbst".
flush ist kein magischer Befehl „speichere alles". Er synchronisiert das, was Hibernate als geändert betrachtet. Wenn es keine Änderungen gibt, produziert flush() möglicherweise überhaupt kein UPDATE. Und umgekehrt: Wenn du versehentlich ein Feld geändert hast (z. B. ein Value Object mutiert), sendet flush ehrlich SQL. Ohne das Dirty-Checking-Modell wirkt das wie Chaos.

Fehler Nr. 4: Schlussfolgerungen aus dem Java-Code ziehen, ohne ins SQL-Log zu schauen.
Hibernate macht vieles zur Laufzeit, und ein erheblicher Teil seines Verhaltens ist nur im SQL sichtbar. Im Code siehst du „ich habe den Namen geändert", im SQL – „in welchem Moment wurde das zum UPDATE". Genau genommen sind das verschiedene Fragen. Wenn du aufhören willst, dem ORM blind zu vertrauen, musst du dich daran gewöhnen: Das SQL-Log ist dein Wahrheitsdetektor.

Fehler Nr. 5: Sich über flush() als „Zuverlässigkeitsknopf" freuen und es überall einsetzen.
Nach der ersten Erleuchtung entsteht manchmal eine neue Religion: „Lass uns nach jeder Änderung flush() machen, dann ist bestimmt alles okay." Das ist ungefähr so, als würde man jedes Problem mit System.out.println() lösen wollen – hilft manchmal, macht aber meistens alles nur langsamer und lauter. flush sollte ein bewusster Synchronisierungspunkt sein, kein Ritual.

1
Aufgabe
Hibernate deep-dive, Level 4, Lektion 0
Gesperrt
Expliziter Flush nach Änderung einer verwalteten Entität
Expliziter Flush nach Änderung einer verwalteten Entität
1
Aufgabe
Hibernate deep-dive, Level 4, Lektion 0
Gesperrt
Flush ist nicht gleich commit
Flush ist nicht gleich commit
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION