CodeGym /Kursy /Hibernate deep-dive /Snapshot i dirty checking w Hibernate

Snapshot i dirty checking w Hibernate

Hibernate deep-dive
Poziom 3, Lekcja 0
Dostępny

1. Jak Hibernate aktualizuje dane

Gdy w logach nagle pojawia się UPDATE, choć w kodzie nie ma ani SQL, ani jawnego „zapisz", pierwsza reakcja to zwykle: „Hibernate żyje własnym życiem". W rzeczywistości wszystko jest bardziej prozaiczne: ma po prostu inny model działania. Nie mówisz mu „zaktualizuj tabelę", lecz zmieniasz obiekt, a Hibernate traktuje to jako część unit of work. Potem sam decyduje, co dokładnie zsynchronizować z bazą.

Jeśli mówisz w terminach stanu obiektu: „dostaję obiekt Product → zmieniam product.setName(...) → na końcu operacji biznesowej ORM synchronizuje zmiany". Żeby to działało, Hibernate musi odpowiedzieć na dwa pytania inżynierskie. Po pierwsze, które obiekty są teraz pod jego zarządzaniem. Po drugie, które z nich naprawdę się zmieniły — i tu właśnie pojawiają się snapshot i dirty checking.

Jeśli bardzo uprościć, obraz wygląda tak: Hibernate robi „zdjęcie" wyjściowego stanu managed-encji i potem porównuje je z bieżącym. Jest różnica — przygotowuje UPDATE. Nie ma różnic — idzie dalej i nie traci SQL-a na próżno.

Termin Ludzkie wytłumaczenie Gdzie żyje
snapshot „Zdjęcie" wyjściowych wartości pól encji w momencie, gdy stała się managed wewnątrz persistence context
dirty checking Porównywanie bieżących wartości ze snapshotem wykonywane przez Hibernate przy synchronizacji z bazą
dirty entity Managed-encja, u której znaleziono różnice ze snapshotem też wewnątrz persistence context

2. Snapshot i dirty checking

Snapshot

Snapshot — to wewnętrzne dane Hibernate, najczęściej zestaw wartości właściwości zapisanych do przyszłego porównania. Ważny moment: snapshot nie wymaga dodatkowego zapytania SQL. Hibernate nie chodzi do bazy, żeby „sfotografować" encję — ma już dane pobrane przy ładowaniu i to właśnie je zachowuje jako linię bazową.

Gdy encja staje się managed (np. przez entityManager.find(Product.class, id)), Hibernate wkłada ją do persistence context. Obok przechowuje informacje pomocnicze: identyfikator, status i snapshot wyjściowych wartości. Dopóki encja pozostaje managed, jest „pod obserwacją".

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"]

Dirty checking

Dirty checking nie wyzwala się w momencie wywołania settera, lecz wtedy, gdy Hibernate zamierza zsynchronizować persistence context z bazą. Idea jest prosta inżyniersko — i przez to niezawodna. Dla każdej managed-encji jest snapshot. Hibernate bierze bieżące wartości pól i porównuje je z nim. Jeśli znajdzie różnicę — encja jest uznana za dirty i Hibernate planuje SQL UPDATE. Jeśli nie ma różnicy — żadnej aktualizacji, nawet jeśli czytałeś pola czy logowałeś obiekt.

import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameProduct(Long id) {
    // Ładujemy encję — od tej chwili jest managed i Hibernate zrobił snapshot
    Product product = entityManager.find(Product.class, id);

    // Zmieniamy pole w pamięci: SQL jeszcze nie jest wysyłany
    product.setName("Keyboard");

    // SQL UPDATE pojawi się nie "w tej sekundzie", lecz przy synchronizacji z bazą (flush/commit)
}

I odwrotnie — samo odczytanie danych nie czyni encji dirty:

import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public String readProductTitle(Long id) {
    // Encja managed, snapshot jest...
    Product product = entityManager.find(Product.class, id);

    // ...ale nic nie zmieniamy: tylko czytamy pola
    return product.getSku() + " " + product.getName();
}

Tu Hibernate nie ma obowiązku niczego aktualizować, bo snapshot nie różni się od bieżących wartości pól.

3. Dirty checking tylko dla managed-encji

Dirty checking — to właściwość stanu managed, a nie „magia repozytorium". Hibernate śledzi tylko to, co jest w persistence context. Reszta to zwykłe obiekty Java.

Stan encji W persistence context? Hibernate trzyma snapshot? Dirty checking może doprowadzić do UPDATE?
transient nie nie nie
managed tak tak tak
detached nie nie nie
removed tak (do synchronizacji) tak nie UPDATE, lecz zwykle DELETE

4. Setter i UPDATE: rola snapshotu

Intuicja „wywołałem setter → będzie UPDATE" jest bardzo ludzka. Ale Hibernate myśli nie zdarzeniami jak „ktoś wywołał metodę", lecz stanami: „jakie były wartości i jakie są teraz". Dlatego sam setter niczego nie gwarantuje. Jeśli przypisałeś tę samą wartość, snapshot i bieżący stan pokryją się — encja nie jest dirty i UPDATE nie jest potrzebny.

import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void setSameValue(Long id) {
    Product product = entityManager.find(Product.class, id);

    // Bierzemy bieżącą wartość...
    String sameName = product.getName();

    // ...i zapisujemy ją z powrotem: pod względem danych nie ma zmian
    product.setName(sameName); // logicznie nic się nie zmieniło
}

Gdy w bazie było " Mouse " (ze spacjami), po trim() stanie się "Mouse". To realna zmiana względem snapshotu — Hibernate uczciwie zrobi UPDATE. Ważna lekcja: Hibernate nie „zgaduje intencji". Widzi fakt: pole się zmieniło lub nie. I działa odpowiednio.

5. Scenariusz: jeden find(), jeden UPDATE

Hibernate porównuje końcowy stan managed-encji ze snapshotem, a nie drukuje UPDATE po każdym twoim działaniu. Dlatego możesz dziesięć razy zmieniać pole w jednej metodzie, a SQL UPDATE będzie jeden — na końcu unit of work — z ostateczną wartością.

import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameProductSeveralTimes(Long id) {
    // Jeden find() — jedna managed-encja, jeden snapshot
    Product product = entityManager.find(Product.class, id);

    // Można zmieniać pole dowolną liczbę razy w ramach transakcji...
    product.setName("Keyboard");
    product.setName("Keyboard Pro");

    // ...ale ostatecznie Hibernate porówna snapshot z końcową wartością
    product.setName("Keyboard Pro Max"); // wartość końcowa
}

Jeśli masz włączony SQL trace, obraz wygląda mniej więcej tak:

-- 1) Najpierw Hibernate ładuje encję (i na tym samym kroku formuje snapshot)
select p1_0.id, p1_0.name, p1_0.sku
from product p1_0
where p1_0.id = ?

-- 2) Potem, przy synchronizacji (flush/commit), wysyła UPDATE jeśli znalazł różnice
update product
set name = ?
where id = ?

6. Typowe błędy przy pracy ze snapshotem i dirty checking

Błąd nr 1: myślenie, że dirty checking działa dla obiektów detached.
Po detach() (lub po zakończeniu transakcji) obiekt staje się zwykłą instancją Java bez nadzoru. Możesz zmieniać jego pola do woli, ale do bazy to nie trafi.

Błąd nr 2: postrzeganie snapshotu jako „drugiego obiektu, którego można używać w kodzie".
Snapshot — to wewnętrzne dane Hibernate, a nie DTO. Do logiki domenowej nie jest przeznaczony.

Błąd nr 3: oczekiwanie SQL od razu po setterze i dziwienie się, że go nie ma.
Hibernate oddziela zmianę obiektu w pamięci od wysyłania SQL do bazy. Dirty checking tylko wykrywa zmiany, a sam SQL pojawia się przy synchronizacji unit of work z bazą.

Błąd nr 4: uważanie, że każde wywołanie settera nieuchronnie tworzy UPDATE.
Setter — to zdarzenie w twoim kodzie. Hibernate pracuje na zasadzie „porównaliśmy wartości". Jeśli nowa wartość zgadza się ze snapshotem, encja nie stanie się dirty.

Błąd nr 5: szukanie UPDATE wyłącznie tam, gdzie jest save() w kodzie.
UPDATE może pojawić się bez save(), jeśli pracowałeś z managed-encją. Najlepsze pytanie na start diagnostyki: „Czy obiekt był managed? I które pole naprawdę się zmieniło względem snapshotu?"

1
Zadanie
Hibernate deep-dive, poziom 3, lekcja 0
Niedostępne
Automatyczna aktualizacja zarządzanego produktu
Automatyczna aktualizacja zarządzanego produktu
1
Zadanie
Hibernate deep-dive, poziom 3, lekcja 0
Niedostępne
Zmiana po `detach()` bez zapisu do bazy danych
Zmiana po `detach()` bez zapisu do bazy danych
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION