1. Wie Hibernate Daten aktualisiert
Wenn in den Logs plötzlich ein UPDATE auftaucht, obwohl im Code weder SQL noch ein explizites „Speichern" steht, ist die erste Reaktion meistens: „Hibernate macht, was es will." In Wirklichkeit ist es nüchterner: Es hat einfach ein anderes Arbeitsmodell. Du sagst ihm nicht „aktualisiere die Tabelle", sondern änderst das Objekt, und Hibernate betrachtet das als Teil der Unit of Work. Danach entscheidet es selbst, was genau mit der Datenbank synchronisiert werden muss.
Wenn du JDBC schreibst oder SQL manuell zusammenbaust, denkst du in Befehlen: „Ich möchte den Produktnamen ändern → ich schreibe UPDATE product set name=?". In der ORM-Welt verlagert sich der Fokus auf den Zustand des Objekts: „Ich hole das Product-Objekt → ändere es mit product.setName(...) → am Ende der Geschäftsoperation synchronisiert das ORM die Änderungen." Damit das funktioniert, muss Hibernate zwei Engineering-Fragen beantworten. Erstens: Welche Objekte stehen gerade unter seiner Verwaltung – das betrifft managed und den Persistence Context. Zweitens: Welche davon haben sich wirklich verändert – und genau hier kommen Snapshot und Dirty Checking ins Spiel.
Wir haben managed-Entitäten bereits von detached getrennt und gesehen, dass der Persistence Context Objekte nicht „der Schönheit halber" hält, sondern als arbeitenden Teil der Anwendung zur Laufzeit. Jetzt wird das ganz praktisch: Solange eine Entität managed ist, kann Hibernate ihren aktuellen Zustand mit dem ursprünglichen vergleichen und entscheiden, ob überhaupt ein UPDATE nötig ist.
Ganz vereinfacht sieht das Bild so aus: Hibernate macht ein „Foto" des ursprünglichen Zustands der managed-Entität und vergleicht es später mit dem aktuellen. Gibt es einen Unterschied – wird ein UPDATE vorbereitet. Gibt es keinen Unterschied – wird weitergemacht, ohne SQL zu verschwenden.
Damit wir nicht mehr raten müssen, führen wir Begriffe ein, mit denen wir ruhig arbeiten können:
| Begriff | Verständliche Erklärung | Wo er lebt |
|---|---|---|
| Snapshot | „Momentaufnahme" der ursprünglichen Feldwerte einer Entität zum Zeitpunkt, als sie managed wurde | innerhalb des Persistence Context |
| Dirty Checking | Vergleich der aktuellen Werte mit dem Snapshot | wird von Hibernate bei der Synchronisierung mit der DB ausgeführt |
| Dirty Entity | Eine managed-Entität, bei der Abweichungen vom Snapshot gefunden wurden | ebenfalls innerhalb des Persistence Context |
| Automatic Change Detection | Grundidee: „Hibernate entscheidet selbst, ob ein UPDATE nötig ist" | das ist ORM-Verhalten, kein separates API |
2. Snapshot und Dirty Checking
Snapshot
Das Wort Snapshot legt leicht den Gedanken nahe, Hibernate erstelle irgendwo ein „zweites Product" und halte es bereit. In Wirklichkeit ist das weder dein vollständiges Objekt noch etwas, mit dem du direkt arbeitest. Ein Snapshot sind interne Daten von Hibernate – meistens ein Satz gespeicherter Eigenschaftswerte für künftige Vergleiche. Und ein wichtiger Punkt: Ein Snapshot erfordert keine zusätzliche SQL-Anfrage. Hibernate geht nicht in die Datenbank, um die Entität zu „fotografieren" – es hat die Daten beim Laden bereits erhalten und speichert genau diese als Baseline.
Wenn eine Entität managed wird (zum Beispiel über entityManager.find(Product.class, id) oder über eine Abfrage select p from Product p), legt Hibernate sie in den Persistence Context. Daneben speichert es Dienstinformationen: Bezeichner, Status und ja – den Snapshot der ursprünglichen Werte. Solange die Entität managed bleibt, steht sie „unter Beobachtung".
Es ist hilfreich, sich den Persistence Context nicht nur als eine Zuordnung id → Objekt vorzustellen, sondern als Struktur, in der es für jede managed-Entität eine eigene „Karteikarte" gibt:
flowchart LR PC["Persistence Context"] --> E["Managed Product #42"] PC --> S["Snapshot for Product #42
name='Mouse'
price=49.99"] E --> DC["Dirty checking"] S --> DC DC -->|diff| SQL["Schedule UPDATE"]
Wie Hibernate den Snapshot intern speichert, spielt hier keine Rolle: Es gibt viele interne Details, die wir gerade nicht brauchen. Wichtig ist, das Wesentliche festzuhalten: Ein Snapshot ist Teil der Mechanik des Persistence Context, keine „deine Kopie des Objekts".
Um Verwechslungen zu vermeiden, hilft eine einfache Prüfung: Wenn du das direkt in deinem Domänencode abrufen und verwenden kannst, ist es kein Hibernate-Snapshot. Der Hibernate-Snapshot ist intern: Er steckt „im Motor", nicht „auf dem Armaturenbrett".
Dirty Checking
Dirty Checking wird nicht beim Aufruf eines Setters ausgelöst, sondern dann, wenn Hibernate dabei ist, den Persistence Context mit der Datenbank zu synchronisieren. Genau in diesem Moment hört es auf, ein bloßer „intelligenter Objektcontainer" zu sein, und beginnt zu entscheiden, welches SQL gebraucht wird. Eine ausführliche Diskussion über Flush brauchen wir noch nicht; es reicht, die Grundmechanik festzuhalten: Vergleich Snapshot ↔ aktueller Zustand.
Die Idee ist engineering-langweilig – also zuverlässig. Jede managed-Entität hat einen Snapshot. Hibernate nimmt die aktuellen Feldwerte und vergleicht sie damit. Findet es einen Unterschied, gilt die Entität als dirty, und Hibernate plant ein SQL-UPDATE. Gibt es keinen Unterschied – kein Update, auch wenn du Felder gelesen, das Objekt geloggt oder einfach lange darauf geschaut hast (das zählt leider nicht als Änderung).
Pseudocode (kein echtes Hibernate-API, sondern „wie man es denken soll"):
// Pseudocode: Illustration der Idee, nicht für den Produktivbetrieb gedacht :)
for (ManagedEntity entity : persistenceContext) {
// Snapshot — die "Baseline": Werte zum Zeitpunkt, als die Entität managed wurde
Object[] snapshot = entity.getSnapshot();
// Aktueller Zustand — Feldwerte jetzt (nach deinen setXxx()-Aufrufen)
Object[] current = entity.getCurrentState();
// Hibernate vergleicht nach Eigenschaften: bei Unterschieden wird ein UPDATE geplant
if (!equalsByProperties(snapshot, current)) {
scheduleUpdate(entity); // SQL wird später ausgeführt, bei der Synchronisierung (flush)
}
}
Jetzt ein minimales Beispiel in unserem Projekt Commerce Persistence Lab. Angenommen, wir ändern den Produktnamen:
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void renameProduct(Long id) {
// Entität laden — ab diesem Moment ist sie managed, und Hibernate hat einen Snapshot erstellt
Product product = entityManager.find(Product.class, id);
// Feld im Speicher ändern: SQL wird noch nicht abgeschickt
product.setName("Keyboard");
// Das SQL UPDATE erscheint nicht "in dieser Sekunde", sondern bei der Synchronisierung mit der DB (normalerweise bei flush/commit)
}
Hier ist eines entscheidend: product ist managed. Das heißt, Hibernate hat bereits einen Snapshot gespeichert (z. B. name="Mouse"). Du hast das Feld geändert. Bis die Transaktion abgeschlossen ist, ist das nur eine Objektänderung im Speicher. Aber gegen Ende der Unit of Work vergleicht Hibernate name mit dem Snapshot, stellt einen Unterschied fest und bereitet ein UPDATE product set name=? where id=? vor.
Und ein Gegenbeispiel: Das Lesen von Daten macht eine Entität nicht dirty:
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public String readProductTitle(Long id) {
// Entität ist managed, Snapshot ist vorhanden...
Product product = entityManager.find(Product.class, id);
// ...aber wir ändern nichts: wir lesen nur Felder
return product.getSku() + " " + product.getName();
}
Hier muss Hibernate nichts aktualisieren, weil der Snapshot mit den aktuellen Feldwerten übereinstimmt. Wir haben nur Daten gelesen. Das ORM ist kein Hellseher: Wenn du nichts verändert hast, gibt es nichts zu synchronisieren.
3. Dirty Checking gilt nur für Managed-Entitäten
Genau hier bricht die Logik von Anfängern am häufigsten zusammen: „Ich halte das Objekt in einer Variablen, also sieht Hibernate es." Leider nein. Hibernate ist kein Wahrsager und liest deine lokalen Variablen nicht. Es verfolgt nur, was sich im Persistence Context befindet, also managed-Entitäten. Alles andere sind normale Java-Objekte: Ändere sie, wirf sie weg – Hibernate wird nicht einmal beleidigt sein. Es erfährt einfach nichts davon.
Um nicht zu raten, ist es am einfachsten, EntityManager.contains(entity) zu verwenden. Das ist ein guter „Test auf Managed-Status", der nicht selten Stunden spart.
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void checkManaged(Long id) {
Product product = entityManager.find(Product.class, id);
// Prüfen, ob das Objekt unter der Verwaltung des Persistence Context steht (managed)
System.out.println(entityManager.contains(product)); // true
}
Nun ein Beispiel mit detach(). Gestern half es, die Entitätszustände zu analysieren; heute wird es gebraucht, um die Grenze des Dirty Checking zu ertasten:
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void changeAfterDetach(Long id) {
Product product = entityManager.find(Product.class, id);
// Entität explizit aus dem Persistence Context entfernen: sie ist nicht mehr managed
entityManager.detach(product);
// Hibernate verfolgt keine Änderungen an diesem Objekt mehr
System.out.println(entityManager.contains(product)); // false
// Feld ändern — aber Dirty Checking greift nicht, kein UPDATE wird geplant
product.setName("Temporary name"); // Hibernate ignoriert das
}
Nach detach() wird das Objekt zu detached. Es liegt noch in deinem Speicher, du kannst seine Felder ändern, aber Snapshot und Dirty Checking funktionieren nicht mehr für dieses Objekt, weil Hibernate aufgehört hat, es als Teil der aktuellen Unit of Work zu betrachten.
Wenn man sich das „auf Fingern" merken möchte, hier eine kompakte Tabelle:
| Zustand der Entität | Im Persistence Context? | Speichert Hibernate einen Snapshot? | Kann Dirty Checking zu einem UPDATE führen? |
|---|---|---|---|
| transient | nein | nein | nein |
| managed | ja | ja | ja |
| detached | nein | nein | nein |
| removed | ja (bis zur Synchronisierung) | ja | kein UPDATE, sondern normalerweise DELETE (zum richtigen Zeitpunkt) |
Und hier zeigt sich der erste Anti-Mythos: Dirty Checking ist keine „Magie des Repositorys", sondern eine Eigenschaft des managed-Zustands.
4. Setter und UPDATE: die Rolle des Snapshots
Die Intuition „Setter aufgerufen → also kommt ein UPDATE" ist sehr menschlich. Aber Hibernate denkt nicht in Ereignissen wie „jemand hat eine Methode aufgerufen", sondern in Zuständen: „Welche Werte waren da, und welche sind jetzt da?" Daher garantiert ein Setter allein gar nichts. Wenn du denselben Wert gesetzt hast, stimmen Snapshot und aktueller Zustand überein – die Entität ist nicht dirty, und kein UPDATE ist nötig.
Ein einfaches Beispiel:
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void setSameValue(Long id) {
Product product = entityManager.find(Product.class, id);
// Aktuellen Wert holen...
String sameName = product.getName();
// ...und wieder zuweisen: aus Datensicht gibt es keine Änderung
product.setName(sameName); // logisch wurde nichts verändert
}
Wenn sameName tatsächlich dem aktuellen Wert entspricht, sieht Hibernate keinen Unterschied. In den Logs (bei korrekter SQL-Trace-Konfiguration) siehst du ein SELECT, aber aufgrund dieser Zeile kein UPDATE.
Eine lebensnähere Variante, die oft überrascht (und manchmal zu einem accidental update führt, dem eine eigene Lektion gewidmet ist): Du führst eine „Normalisierung" eines Strings durch.
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void normalizeName(Long id) {
Product product = entityManager.find(Product.class, id);
// trim() kann den Wert im Vergleich zum Snapshot wirklich verändern (wenn die DB Leerzeichen enthielt)
product.setName(product.getName().trim());
}
Wenn in der Datenbank " Mouse " (mit Leerzeichen) stand, wird es nach trim() zu "Mouse". Das ist eine echte Änderung gegenüber dem Snapshot – Hibernate führt ehrlich ein UPDATE aus. Wenn in der Datenbank aber bereits "Mouse" ohne Leerzeichen stand, gibt trim() denselben Wert zurück, und kein UPDATE erscheint.
Hier ist es wichtig, nicht durcheinanderzukommen: Hibernate „errät keine Absichten". Es weiß nicht, dass du „den String nur gesäubert hast". Es sieht die Tatsache: Das Feld ist anders geworden oder nicht. Und es handelt entsprechend.
Daraus ergibt sich eine nützliche Schlussfolgerung für die Diagnose: Wenn du ein unerwartetes UPDATE siehst, suche nicht nach „wo jemand save aufgerufen hat", sondern nach „wo der Wert im Vergleich zum Snapshot wirklich anders geworden ist". Hibernate liegt fast immer innerhalb seines Modells im Recht. Die Frage ist nur, warum die Änderung stattfand, wenn du sie nicht geplant hattest.
5. Szenario: ein find(), ein UPDATE
Damit das Snapshot-Modell keine Abstraktion bleibt, ist es hilfreich, ein praktisches Detail zu sehen: Hibernate vergleicht den endgültigen Zustand der managed-Entität mit dem Snapshot, nicht nach jeder einzelnen Aktion. Daher kannst du ein Feld zehnmal in einer Methode ändern, und es gibt trotzdem nur ein einziges SQL-UPDATE – am Ende der Unit of Work – mit dem Endwert.
Beispiel: Du änderst den Produktnamen mehrfach (im echten Leben macht man das normalerweise nicht so, aber zur Demo ist es perfekt).
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void renameProductSeveralTimes(Long id) {
// Ein find() — eine Managed-Entität, ein Snapshot
Product product = entityManager.find(Product.class, id);
// Das Feld kann beliebig oft innerhalb der Transaktion geändert werden...
product.setName("Keyboard");
product.setName("Keyboard Pro");
// ...aber am Ende vergleicht Hibernate den Snapshot mit dem Endwert
product.setName("Keyboard Pro Max"); // Endwert
}
Aus Java-Sicht hast du das Feld dreimal geändert. Aus Sicht von Hibernate war der Snapshot zum Beispiel "Mouse", und der Endzustand ist "Keyboard Pro Max". Es gibt einen Unterschied – also ist ein UPDATE nötig. Aber nur eines, weil die Synchronisierung am Ende der Unit of Work als Batch stattfindet.
Wenn der SQL-Trace aktiviert ist, sieht das Bild normalerweise ungefähr so aus (vereinfacht, ohne Bind-Parameter):
-- 1) Zuerst lädt Hibernate die Entität (und erstellt dabei den Snapshot)
select p1_0.id, p1_0.name, p1_0.sku
from product p1_0
where p1_0.id = ?
-- 2) Dann, bei der Synchronisierung (flush/commit), schickt es ein UPDATE, wenn es Unterschiede fand
update product
set name = ?
where id = ?
Beachte: SQL muss nicht „klein" sein. Je nach Mapping kann Hibernate ein UPDATE erzeugen, das mehr Spalten aktualisiert als erwartet. Jetzt interessiert uns nur das Grundprinzip, warum ein UPDATE erscheint. Warum SQL manchmal breiter als erwartet ausfällt und woher versehentliche Updates kommen – das ist bereits die nächste Analyseebene.
Noch ein kleines, aber wichtiges Detail: Nach einer erfolgreichen Synchronisierung aktualisiert Hibernate seinen Snapshot, weil der neue Zustand zur „neuen Baseline" wird. Das bedeutet, ein Snapshot ist keine einmalige Aufnahme „für das ganze Leben der Entität", sondern ein Arbeitswerkzeug des jeweiligen Persistence Context.
6. Typische Fehler beim Arbeiten mit Snapshot und Dirty Checking
Fehler Nr. 1: Glauben, Dirty Checking funktioniere für detached-Objekte.
Die Falle „Ich habe das Objekt, also sieht Hibernate es" kommt ständig vor. Aber nach detach() (oder nach dem Ende einer Transaktion, wenn der Persistence Context geschlossen ist) wird das Objekt zu einer normalen Java-Instanz ohne Überwachung. Du kannst es beliebig ändern, aber diese Änderungen landen nicht von allein in der Datenbank, weil Snapshot und Vergleich nur innerhalb des Persistence Context leben.
Fehler Nr. 2: Den Snapshot als „zweites Objekt betrachten, das man im Code nutzen kann".
Ein Snapshot ist kein Product copy, kein DTO und keine „Version vor den Änderungen", die man dem Nutzer zeigen könnte. Es sind interne Strukturen von Hibernate, nicht für die Domänenlogik gedacht. Wenn du wirklich eine „Vorher-Nachher"-Version für das Geschäft oder Auditing brauchst – das ist eine separate Aufgabe (später haben wir Envers). Aber das ist nicht der Zweck des Snapshots.
Fehler Nr. 3: Sofort SQL nach dem Setter erwarten und sich wundern, dass es nicht da ist.
Hibernate trennt die Objektänderung im Speicher vom Abschicken des SQL an die Datenbank. Dirty Checking erkennt nur Änderungen, und das SQL selbst erscheint erst bei der Synchronisierung der Unit of Work mit der DB. Daher siehst du innerhalb einer Methode vielleicht gar keine Aktivität, und das ist normal. Hauptsache: Diese beiden Phasen nicht vermischen.
Fehler Nr. 4: Glauben, jeder Setter-Aufruf erzeuge zwingend ein UPDATE.
Ein Setter ist ein Ereignis in deinem Code. Hibernate hingegen arbeitet nach dem Prinzip „Werte verglichen". Wenn der neue Wert dem Snapshot entspricht, wird die Entität nicht dirty. Das ist besonders nützlich zu wissen, wenn du Strings „normalisierst" oder Zahlen rundest: Manchmal änderst du die Daten tatsächlich, auch wenn es dir wie bloße „Kosmetik" vorkommt.
Fehler Nr. 5: UPDATE nur am Vorhandensein von save() im Code festmachen.
Die Gewohnheit zu denken „geändert → save aufgerufen" verdeckt oft die eigentliche Mechanik. Ein UPDATE kann auch ohne save() erscheinen, wenn du mit einer managed-Entität gearbeitet hast; und umgekehrt: save() ist kein magischer „Muss-unbedingt-updaten"-Knopf, wenn du nicht verstehst, in welchem Zustand das Objekt ist. Die beste Ausgangsfrage für die Diagnose: „War das Objekt managed? Und welches Feld hat sich wirklich im Vergleich zum Snapshot verändert?"
GO TO FULL VERSION