1. Flush ist ein Paket von Änderungen, kein „einzelner SQL-Befehl"
Wenn man anfängt, das Hibernate-SQL-Log zu lesen, stellt man sich flush leicht als einen einzelnen Knopf vor: Drücken – ein Befehl fliegt raus, wie git commit, nur für die Datenbank. Leider (oder zum Glück) ist flush eher ein „Stapel von Aufgaben", den Hibernate angesammelt hat, während du in Ruhe Objekte im Speicher geändert hast. Und dieser Stapel kann gleichzeitig mehrere INSERT, mehrere UPDATE, mehrere DELETE und manchmal auch Operationen an Beziehungen enthalten.
Wenn die Frage „wann flush auslöst" schon einigermaßen klar ist, lohnt es sich, einen Blick in den Synchronisierungsvorgang selbst zu werfen. Ein einzelner flush kann ein ganzes Paket von Operationen in die Datenbank schicken – und genau daraus entsteht das Gefühl: Warum geht SQL nicht in der Reihenfolge, in der ich meinen Java-Code geschrieben habe?
Technisch gesehen häuft Hibernate im Laufe der Transaktion Änderungen in einer Art „Aktionswarteschlange" an. Du könntest persist() aufgerufen haben, ein Feld per Setter geändert haben, remove() aufgerufen haben oder überhaupt nichts explizit – und Dirty Checking hat das Objekt selbst als geändert markiert. All das wird nicht zu sofortigem SQL, sondern zu einem „Plan": Was soll eingefügt, aktualisiert, gelöscht werden? Und wenn der flush-Moment kommt (automatisch oder explizit), nimmt Hibernate diesen Plan und fängt an, ihn auszuführen.
Daraus folgt der erste wichtige Schluss dieser Vorlesung: Innerhalb eines flush gibt es fast immer mehrere SQL-Operationen, und Hibernate wird versuchen, sie so auszuführen, dass die Datenbank nicht mit Fehlern über Foreign Keys, Unique-Constraints und anderen Integritätsregeln reagiert. Das heißt: Hibernate denkt nicht in der Reihenfolge, in der du Java-Zeilen geschrieben hast, sondern in der Reihenfolge, in der die Datenbank die Änderungen akzeptiert.
Um diese Idee greifbar zu machen, schauen wir uns ein Mini-Szenario an (das Beispiel ist bewusst kurz und didaktisch, nutzt aber unsere Domäne):
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void flushIsAPackage(EntityManager em) {
// 1) Entität laden: sie wird managed im Persistence Context
Product product = em.find(Product.class, 1L);
// 2) Managed-Objekt ändern: das ist kein SQL jetzt, sondern ein zukünftiges UPDATE beim Flush
product.setName("Phone Pro");
// 3) Andere Entität laden und zum Löschen markieren
Customer customer = em.find(Customer.class, 1L);
em.remove(customer); // zukünftiges DELETE beim Flush (manchmal auch ein SQL-Paket wegen Beziehungen)
// 4) Flush explizit aufrufen: Hibernate sendet den angesammelten „Plan" an die DB
em.flush(); // darin können sowohl UPDATE als auch DELETE enthalten sein
}
In diesem Code landen zwei verschiedene Operationen in einem einzigen flush. Und jetzt stellt sich nicht mehr die Frage „welche Zeile ist weiter oben", sondern: „Was kann gelöscht werden, ohne Constraints zu verletzen?", „Gibt es Referenzen auf customer?", „Muss product jetzt aktualisiert werden?" usw. Die Reihenfolge wird durch die interne Synchronisierungslogik bestimmt – nicht durch deinen Schönheitssinn.
2. Java-Reihenfolge und SQL-Reihenfolge beim Flush
Wenn ein Entwickler zum ersten Mal eine „falsche SQL-Reihenfolge" sieht, vermutet er oft, Hibernate habe „versehentlich etwas verwechselt". Tatsächlich tut Hibernate genau das, was es tun muss: Es synchronisiert den Zustand der Objekte mit der Datenbank in der korrekten Reihenfolge. Und Korrektheit wird hier nicht durch den Codestil definiert, sondern durch die Regeln des relationalen Modells.
Java-Code schreibt man wie eine Geschichte: „Erst habe ich den Kunden gelöscht, dann das Produkt geändert." Hibernate schaut beim flush auf die Menge der aufgelaufenen Aktionen und entscheidet, wie es sie anordnet. Gründe für eine Neuordnung gibt es mehrere, und sie sind recht prosaisch. Der häufigste: Foreign Keys. Der zweithäufigste: flush gruppiert Operationen nach Typ – Inserts separat, Updates separat, Deletes separat. Der dritte (etwas seltener, aber trotzdem wichtig): Hibernate versucht, SQL nicht „im Zickzack" zu senden, weil das der Datenbank das Leben schwer macht und JDBC-Optimierungen behindern kann.
Lass uns das typische Bild nachbilden, das die Intuition bricht: Du erstellst eine Bestellposition, aber die Bestellung selbst speicherst du „später". In Java kann das so aussehen (das Beispiel ist absichtlich etwas „unschön" aus Domain-Logik-Sicht, um das Reihenfolge-Problem hervorzuheben):
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void javaOrderIsNotSqlOrder(EntityManager em) {
// Die Bestellung existiert bereits in der DB, also ist der FK darauf potenziell gültig
PurchaseOrder order = em.find(PurchaseOrder.class, 1L);
// Wir erstellen ein „Kind", das über FK auf den „Elternteil" zeigt
OrderItem item = new OrderItem();
item.setOrder(order); // FK auf order (order_id)
item.setQuantity(2);
// persist bedeutet nicht „INSERT jetzt": es stellt in den Plan ein
em.persist(item);
// Beim flush fliegt INSERT in order_item raus
em.flush();
}
Auch wenn es so aussieht, als ob „ich habe item zuerst erstellt, also kommt INSERT item zuerst" – die Datenbank stimmt nicht zu: Die Zeile order_item referenziert purchase_order über einen Foreign Key. Deshalb muss Hibernate sicherstellen, dass die Referenz gültig ist. In unserem Beispiel existierte die Bestellung bereits – also ist das Einfügen von order_item möglich. Hätten wir aber eine neue Bestellung und neue Positionen in einer Transaktion erstellt, hätte Hibernate die Bestellung so einfügen müssen, dass order_id existiert und die Constraints erfüllt.
Damit ergibt sich die wichtigste Regel: Die SQL-Reihenfolge beim flush ist die Reihenfolge der Abhängigkeitserfüllung – nicht die Reihenfolge deiner Gedanken.
3. Foreign Keys und Operationsreihenfolge
Das Wort „Foreign Key" klingt nach DBA und Migrationen, aber für das Verständnis von flush ist es eine ganz alltägliche Sache. Ein Foreign Key ist eine Regel: „Tabelle A hat eine Spalte, die auf eine existierende Zeile in Tabelle B verweisen muss." Das heißt, die Datenbank speichert nicht nur Zahlen, sondern achtet auch darauf, dass Referenzen nicht ins Leere führen.
In unserem Lehrprojekt gibt es viele solche Abhängigkeiten, weil die Domäne „Katalog → Bestellungen → Positionen → Lagerbestand" natürlich relational ist. Beispielsweise referenziert OrderItem sowohl PurchaseOrder als auch Product. PurchaseOrder referenziert Customer. InventoryItem referenziert Product. Eine grobe Abhängigkeitskarte sieht ungefähr so aus:
flowchart LR Customer -->|FK customer_id| PurchaseOrder PurchaseOrder -->|FK order_id| OrderItem Product -->|FK product_id| OrderItem Product -->|FK product_id| InventoryItem
Dieses Schema ist nicht um seiner selbst willen wichtig, sondern weil es erklärt, warum Hibernate so hartnäckig ist. Wenn ein einzelner flush Aktionen über mehrere Entitäten enthält, muss Hibernate sie so ausführen, dass jede FK-Referenz zum Zeitpunkt der Ausführung des jeweiligen SQL gültig ist.
Daraus ergibt sich eine recht praktische Gedankenhilfe (die man im Kopf behalten kann, ohne alle JPA-Annotationen auswendig zu kennen). Für INSERT gilt üblicherweise: Der „Elternteil" muss vor dem „Kind" existieren. Für DELETE gilt das Gegenteil: Das „Kind" muss vor dem „Elternteil" verschwinden, sonst lässt die Datenbank die Zeile, auf die noch jemand verweist, nicht löschen. Für UPDATE hängt es davon ab, ob du normale Felder aktualisierst oder Referenzen änderst (z. B. OrderItem in eine andere Bestellung verschiebst).
Vergleich in einer kompakten Tabelle (vereinfacht, aber mit der richtigen Intuition):
| Operation | Was die Datenbank will | Typische Folge für die Reihenfolge |
|---|---|---|
| INSERT Kind mit FK | „Referenz muss auf eine existierende Zeile zeigen" | erst Elternteil einfügen, dann Kind |
| DELETE Elternteil | „Zeile, auf die Referenzen existieren, kann nicht gelöscht werden" | erst Kinder löschen, dann Elternteil |
| UPDATE FK-Spalte | „Neue Referenz muss gültig sein" | neuen Elternteil einfügen (wenn neu erstellt) → Kind aktualisieren |
Und jetzt wird klar, warum Hibernate manchmal wie ein „hartnäckiger Zustimmungsmanager" wirkt: Es macht SQL nicht in der Reihenfolge deiner Zeilen, sondern in der Reihenfolge, die die Referenzintegrität nicht zerstört.
4. Reihenfolge von INSERT beim Flush
Wenn es um Einfügungen geht, möchte man als Einsteiger eine einfache Regel: „Ich habe persist() aufgerufen – also kommt jetzt INSERT." Aber wir haben uns schon geeinigt, dass persist() meistens nur das Objekt in den Plan stellt und INSERT beim flush abgeht. Fügen wir jetzt eine zweite Schicht hinzu: Selbst wenn flush eingetreten ist, kann Hibernate INSERT nicht in der Reihenfolge ausführen, in der du die Objekte erstellt hast.
Im Kontext unserer Domäne ist das anschaulichste Beispiel das Erstellen einer Bestellung und ihrer Positionen. Die Bestellung (PurchaseOrder) ist der „Elternteil", die Positionen (OrderItem) sind die „Kinder", die darauf verweisen. Wenn du in einem einzigen flush sowohl die Bestellung als auch die Positionen erstellst, erwartet die Datenbank, dass die Bestellung zuerst erscheint. Selbst wenn du in Java zuerst die Positionen und dann die Bestellung erstellt hast (was sich schon anhört wie „zuerst der Schwanz, dann die Katze" – aber Java lässt das zu), wird Hibernate trotzdem den Elternteil vor den Kindern einfügen.
Stell dir eine realistische Situation vor: Du erstellst eine Bestellung und fügst sofort Positionen hinzu. Selbst wenn du alles „schön" im Code machst, wird die SQL-Reihenfolge immer noch durch Abhängigkeiten bestimmt:
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void insertOrderWithItems(EntityManager em) {
// Neuen „Elternteil" erstellen (existiert vorerst nur im Speicher)
PurchaseOrder order = new PurchaseOrder();
order.setOrderNumber("ORD-1001");
// „Kind" erstellen und mit dem Elternteil verknüpfen (FK erscheint im INSERT order_item)
OrderItem item = new OrderItem();
item.setOrder(order);
item.setQuantity(1);
// persist() bildet den Einfügungsplan, garantiert aber keine 1:1-Entsprechung von Aufrufreihenfolge zu SQL
em.persist(order);
em.persist(item);
// Beim Flush sendet Hibernate INSERTs so, dass FKs gültig sind
em.flush();
}
Was erwartest du im SQL-Log zu sehen? Üblicherweise so etwas wie „INSERT order, INSERT item". Das ist logisch: order_item hat einen FK auf purchase_order. Der wichtige Punkt: Selbst wenn du die persist()-Aufrufe vertauschst oder gar persist(order) vergisst (in manchen Mappings kann die Einfügung der Bestellung kaskadiert ablaufen – darauf verlassen wir uns hier aber nicht), wird Hibernate trotzdem versuchen, abhängige Entitäten korrekt einzufügen. Deine „Reihenfolge der persist()-Aufrufe" ist kein Vertrag für die SQL-Reihenfolge.
Zur einfacheren Lesbarkeit stellen wir uns ein typisches SQL-Fragment vor (Pseudo-Log, aber sehr ähnlich zur Realität):
-- flush begins
-- Erst wird der „Elternteil" eingefügt, damit eine ID existiert und das „Kind" einen gültigen FK hat
insert into purchase_order (order_number, ...) values ('ORD-1001', ...);
-- Dann wird das „Kind" eingefügt, mit order_id (oder anderem FK) auf die bereits existierende Zeile
insert into order_item (order_id, quantity, ...) values (123, 1, ...);
-- flush ends
Dazu gehört noch ein „feinerer" Fall: OrderItem referenziert nicht nur PurchaseOrder, sondern auch Product. Wenn du in einer Transaktion ein neues Produkt erstellst und es sofort als Position in einer Bestellung einfügst, muss Hibernate zuerst Product einfügen, dann PurchaseOrder (oder umgekehrt – je nachdem, wer von wem abhängt), und erst dann OrderItem, weil es zwei Referenzen hat.
Solche Situationen liest man am besten als „Abhängigkeitsgraph" – nicht als Liste von Codezeilen.
5. Reihenfolge von UPDATE beim Flush
Mit Aktualisierungen ist es üblicherweise einfacher in dem Sinne, dass UPDATE keine neuen Zeilen erstellt und keine alten löscht. Aber „einfacher" bedeutet nicht „immer offensichtlich". Beim flush muss Hibernate zuerst verstehen, was aktualisiert werden soll (Dirty Checking) und dann entscheiden, wann genau UPDATE relativ zu anderen Operationen gesendet wird.
Das häufigste Szenario in unserem Projekt: Du hast eine Managed-Entität geladen und ihre Felder geändert. Zum Beispiel den Produktnamen oder den Bestellstatus. Hibernate markiert die Entität als geändert, und beim flush sendet es UPDATE. Gibt es im flush sonst nichts – alles langweilig und schön.
Spannend wird es, wenn UPDATE neben INSERT oder DELETE lebt. Dann versucht Hibernate, keine Abhängigkeiten zu verletzen. Wenn du z. B. in einer Transaktion eine neue Bestellung erstellst und gleichzeitig ein Produkt aktualisierst, kann UPDATE product vor oder nach INSERT purchase_order kommen – und das liegt nicht daran, dass Hibernate „launisch" ist, sondern daran, dass es zwischen diesen Tabellen möglicherweise keine direkte FK-Abhängigkeit gibt. Fehlt die Abhängigkeit, erhält Hibernate Freiheit, Aktionen nach eigenen Regeln zu gruppieren.
Einfaches Beispiel: In einer Transaktion ändern wir ein Produkt und erstellen eine neue Bestellposition. Beachte: Die Position referenziert das Produkt per FK – also muss das Produkt existieren. Aber das Produkt existiert bereits, wir aktualisieren es nur. Das Einfügen der Position erfordert nicht, dass UPDATE product vorher abgeschlossen ist – die Zeile product muss nur existieren. Daher sind verschiedene Ausführungsreihenfolgen möglich, und das ist normal.
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void updateAndInsert(EntityManager em) {
// Produkt laden: es ist managed, also werden Änderungen von Dirty Checking erfasst
Product p = em.find(Product.class, 1L);
p.setName("Phone Pro"); // zukünftiges UPDATE beim Flush
// Bestellung existiert bereits, also kann die Position sofort darauf verweisen
PurchaseOrder order = em.find(PurchaseOrder.class, 1L);
// Neue Bestellposition erstellen und mit Bestellung und Produkt verknüpfen (FK)
OrderItem item = new OrderItem();
item.setOrder(order);
item.setProduct(p);
// persist – das ist der Plan für INSERT order_item
em.persist(item); // zukünftiges INSERT
// Flush führt sowohl INSERT als auch UPDATE aus; Reihenfolge kann variieren, wenn keine harte Abhängigkeit besteht
em.flush();
}
Im Log kannst du zuerst INSERT order_item und dann UPDATE product sehen – oder umgekehrt. Das sollte dich an sich nicht beunruhigen. Wichtig ist nicht „wer zuerst", sondern dass beide Änderungen in einem einzigen flush enthalten sind und korrekt im Rahmen der Transaktion angewendet wurden.
Eine besondere Feinheit: das Aktualisieren von Beziehungen (d. h. Aktualisierung der FK-Spalte). Wenn du z. B. OrderItem einer anderen Bestellung zuordnest, macht Hibernate ein UPDATE order_item set order_id = .... Und dann kann die Reihenfolge davon abhängen, ob die neue Bestellung bereits in der Datenbank existiert oder gerade in derselben Transaktion erstellt wird. Wird die neue Bestellung in derselben Transaktion erstellt, muss ihr INSERT vor dem UPDATE stattfinden – sonst zeigt der FK in order_item auf eine nicht existierende Zeile. Das ist der Fall, wo UPDATE unerwartet von INSERT abhängt.
6. Wie Hibernate die Reihenfolge von DELETE beim Flush wählt
Löschungen sind der Bereich, wo „Reihenfolge" buchstäblich am eigenen Leib spürbar wird, weil die Datenbank bei falscher Reihenfolge nicht mit Philosophie antwortet, sondern mit einer Exception. Versuchst du, einen Elternteil zu löschen, während noch Kinder mit FK darauf in der Datenbank existieren, sagt PostgreSQL etwas in der Art: „Es ist nicht möglich, eine Zeile zu löschen, auf die Referenzen existieren." Und in diesem Moment dankst du entweder Hibernate dafür, dass es versucht, die Reihenfolge korrekt zu halten – oder bist traurig, dass du über Beziehungen nicht nachgedacht hast.
Das anschaulichste Beispiel: Bestellung und ihre Positionen löschen. Die Positionen haben einen Foreign Key auf die Bestellung (order_id). Um die Bestellung zu löschen, müssen also zuerst die Positionen gelöscht werden – sonst verhindert der FK das.
Im Java-Code kann das sehr „harmlos" aussehen:
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void deleteOrder(EntityManager em) {
// Bestellung laden: sie wird managed
PurchaseOrder order = em.find(PurchaseOrder.class, 1L);
// Zum Löschen markieren: SQL DELETE geht üblicherweise beim Flush raus – nicht sofort jetzt
em.remove(order);
// Beim Flush wird Hibernate versuchen, abhängige Entitäten/Zeilen in der richtigen Reihenfolge zu löschen
em.flush();
}
Auf „Zeilenebene" sieht es aus, als sollte Hibernate ein einzelnes DELETE from purchase_order ... machen. Aber auf Ebene des echten Schemas gibt es fast immer abhängige Zeilen. Deshalb wird Hibernate im flush versuchen, zuerst die Kinder (order_item) zu löschen und dann den Elternteil (purchase_order). Im Log sieht das oft so aus:
-- Erst die „Kinder" löschen, sonst lässt FK das Löschen des Elternteils nicht zu
delete from order_item where order_id = 1;
-- Dann den „Elternteil" löschen, wenn niemand mehr darauf verweist
delete from purchase_order where id = 1;
Und hier gilt es, zwei Gedanken gleichzeitig festzuhalten.
Erster Gedanke: Die DELETE-Reihenfolge im flush ist nicht „so hat Hibernate entschieden", sondern „so fordert es die FK-Integrität." Auch wenn du sehr darum bittest – die Datenbank nimmt „erst Elternteil löschen, dann Kinder" nicht an, weil zum Zeitpunkt des ersten DELETE die Kinder noch existieren und darauf verweisen.
Zweiter Gedanke: Ob Hibernate Kinder überhaupt automatisch löscht, hängt vom Mapping ab (Kaskaden, Orphan Removal, Beziehungskonfiguration) und davon, wie du die Verbindung im Objektmodell getrennt hast. Wir gehen hier nicht tiefer in die Feinheiten, wo man mappedBy setzt und welche Kaskaden man aktiviert – das ist das Ziel einer anderen Vorlesung. Aber du siehst schon: „Eine Entität löschen" bedeutet im ORM fast immer „einen Graph löschen" (oder den Versuch dazu), und die SQL-Reihenfolge wird durch Abhängigkeiten diktiert.
Es gibt noch ein Szenario, das oft überrascht: Du löschst „irgendetwas Einzelnes", und Hibernate macht plötzlich eine Reihe von UPDATE-Befehlen und dann DELETE. Das passiert, wenn vor dem Löschen eine Verbindung aufgelöst oder ein FK auf null gesetzt werden muss (wenn das Schema so aufgebaut ist). Das heißt: Löschen enthält manchmal vorbereitende Schritte. Und nochmals: Die Reihenfolge wird durch die Referenzstruktur und Datenbankconstraints bestimmt.
Gemischter Flush
Ein gemischter flush ist das normale Leben einer echten Service-Methode. In einem Unit of Work kannst du z. B. eine neue Bestellposition erstellen, den Bestellstatus aktualisieren und ein zugehöriges Objekt löschen (oder zum Löschen markieren). Dann wird flush zu einem „Paket von Operationen verschiedener Typen".
Um das „wie es in der Realität aussieht" greifbar zu machen, stell dir vor: Wir haben die Bestellung aktualisiert, eine Position hinzugefügt und eine Kundenadresse gelöscht (wieder ein Lehrbeispiel, keine Domain-Empfehlung).
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void mixedFlush(EntityManager em) {
// 1) UPDATE: Managed-Entität ändern (SQL geht beim Flush raus)
PurchaseOrder order = em.find(PurchaseOrder.class, 1L);
order.setStatus(OrderStatus.CONFIRMED); // UPDATE
// 2) INSERT: Neue Entität erstellen und speichern
OrderItem item = new OrderItem();
item.setOrder(order);
item.setQuantity(1);
em.persist(item); // INSERT
// 3) DELETE: Zum Löschen markieren
CustomerAddress address = em.find(CustomerAddress.class, 10L);
em.remove(address); // DELETE (und möglicherweise vorbereitende UPDATEs wegen Beziehungen)
// 4) Flush: Hier wird das SQL-„Paket" in FK- und Constraint-kompatibler Reihenfolge ausgeführt
em.flush();
}
Was passiert innerhalb des flush? Im Allgemeinen wird Hibernate die Änderungen so ausführen, dass keine FK-Regeln verletzt werden. Häufig ergibt sich eine Reihenfolge wie „Einfügungen → Aktualisierungen → Löschungen", weil Einfügungen Daten erzeugen, Aktualisierungen existierende Daten anpassen und Löschungen das Nicht-mehr-Benötigte entfernen. Aber betrachte das als intuitives Modell, nicht als rechtlich bindenden Vertrag: Die echte Reihenfolge kann davon abhängen, welche FKs und Constraints konkret beteiligt sind.
Das Wichtigste: Lerne flush als „einen Synchronisierungsblock" zu lesen, in dem Hibernate eine konsistente Datenbankänderung durchführt. Siehst du im Log INSERT, dann DELETE, dann UPDATE – versuche das nicht mit „Hibernate ist komisch" zu erklären. Stelle dir lieber die erwachsenen Fragen: „Welche Entitäten waren beteiligt?", „Welche Beziehungen bestehen zwischen ihnen?", „Welche FKs müssen zu jedem Zeitpunkt gültig sein?", „Gab es vorbereitende UPDATE-Befehle vor dem DELETE?"
7. Das SQL-Log rund um den Flush lesen
Das SQL-Log zu lesen ist eine Fähigkeit, die am Anfang daran erinnert, einen Film zu schauen, bei dem du versehentlich den „Drehbuch anzeigen"-Modus eingeschaltet hast. Zu viel Text, unbekannte Tabellen, und select und update kommen so häufig vor, dass man den Laptop zuklappen und ohne ORM in den Wald ziehen möchte. Aber hier gibt es einen Trick: Versuche nicht, das Log wie einen Roman zu lesen. Lies es als flush-Episoden.
Praktisch sieht das so aus: Du findest im Code den Punkt, an dem flush definitiv stattgefunden hat – entweder das explizite entityManager.flush() oder eine Stelle, an der du weißt, dass flush automatisch ausgelöst wird (z. B. vor Commit oder vor einer überlappenden Abfrage). Dann schaust du im Log auf den SQL-Block, der „rund um" diesen Punkt abgegangen ist, und gruppierst ihn gedanklich nach Typ: Einfügungen, Aktualisierungen, Löschungen.
Sehr hilfreich ist das einfache Schema „Objekt → flush → SQL → Commit". In Vorlesung 1 haben wir bereits flush und Commit getrennt, und hier ergänzen wir das Bild: Innerhalb des flush gibt es eine Operationswarteschlange, deren Reihenfolge durch Abhängigkeiten bestimmt wird.
Du kannst dir diese kleine Checkliste merken, um dich nicht täuschen zu lassen:
| Was du im Code siehst | Was das bedeutet | Wie man im SQL prüft |
|---|---|---|
| setName(...) bei Managed-Entität | „Mögliches UPDATE" | suche nach update ... set ... where id = ... beim flush |
| persist(...) | „Zukünftiges INSERT" | suche nach insert into ... beim flush |
| remove(...) | „Zukünftiges DELETE (möglicherweise kaskadiert)" | suche nach delete from ... und möglichen vorbereitenden update-Befehlen |
Und noch eine wichtige Kleinigkeit: Wenn du nach flush ein find() auf dasselbe Objekt ohne clear() machst, kannst du den „neuen Wert" nicht deshalb sehen, weil er aus der Datenbank kam, sondern weil du noch auf dieselbe Managed-Kopie im Speicher schaust. Deshalb ist es bei der Analyse der SQL-Reihenfolge sinnvoll, „was im Speicher ist" und „was in der Datenbank ist" zu trennen. Das ist nicht das Hauptthema dieser Vorlesung, aber dieses Verständnis hilft, keine falschen Schlüsse zu ziehen.
8. Typische Fehler beim Flush
An dieser Stelle möchte man üblicherweise eine Liste mit „10 Einsteiger-Fehlern" schreiben, aber wir gehen humaner vor: Wir besprechen einige Fehler als kleine Geschichten, damit du dich (oder den Kollegen, der „sicher nicht ich" war) wiedererkennst.
Fehler Nr. 1: SQL nach der Reihenfolge der Java-Zeilen erklären wollen.
Das ist die häufigste Falle. Das Gehirn mag lineare Geschichten: „erst A, dann B." Hibernate denkt beim flush nicht in Geschichten, sondern in Abhängigkeiten und Datenintegrität. Deshalb ist die richtige Frage nicht „warum ist SQL nicht in der richtigen Reihenfolge", sondern „welche FK-Constraints und Operationstypen haben Hibernate gezwungen, die Aktionen umzuordnen."
Fehler Nr. 2: Foreign Keys nicht beachten und sich über Constraint-Violations beim Löschen wundern.
Wenn du remove(order) aufrufst und die Datenbank schimpft, zieht der Einsteiger den Schluss: „Hibernate hat die Bestellpositionen nicht gelöscht." Aber meistens liegt das Problem nicht am „Nicht-Löschen", sondern daran, dass du nicht durchdacht hast, welche Zeilen auf was verweisen und was überhaupt ohne Vorbereitung gelöscht werden darf. Löschen ist fast immer mit einem Abhängigkeitsgraph verbunden, und die DELETE-Reihenfolge im flush ist nicht zufällig, sondern erzwungen.
Fehler Nr. 3: Nur auf ein UPDATE/DELETE im Log schauen und den gesamten flush-Block ignorieren.
Flush ist ein Paket. Wenn du mitten im Log ein einzelnes update product herausgreifst und eine Untersuchung startest „warum ist es erschienen", übersiehst du leicht, dass kurz davor ein insert order_item und kurz danach ein delete customer_address war. Der richtige Lesemodus ist, den zusammengehörigen SQL-Block zu betrachten – nicht einzelne Befehle.
Fehler Nr. 4: Denken, dass remove() sofortiges DELETE in der Datenbank bedeutet.
Remove() ändert den Zustand der Entität im Kontext auf removed. Das eigentliche DELETE geht üblicherweise beim flush raus. Und wenn flush nicht dort ausgelöst wird, wo du es erwartest (z. B. früher – vor einer Abfrage), kann DELETE „unerwartet früh" erscheinen. Das ist kein Bug, sondern die Folge der flush-Regeln und der Pflicht von Hibernate, die Korrektheit von Abfragen und Transaktion zu gewährleisten.
Fehler Nr. 5: „Was in der Datenbank ist" mit „Was im Persistence Context ist" verwechseln.
Manchmal macht ein Entwickler setStatus(CONFIRMED), dann find() und sieht den neuen Status – und schließt: „Also ist das Update schon in der Datenbank." Tatsächlich kann das einfach dieselbe Managed-Entität im Speicher sein. Um ehrlich zu prüfen, ob die Datenbank die Änderungen wirklich erhalten hat, braucht man einen flush-Punkt und (oft) ein clear() vor dem erneuten Lesen – sonst prüft man nicht die Datenbank, sondern den eigenen Anwendungsspeicher.
GO TO FULL VERSION