1. Flush mode: autozapis dla bazy
Jeśli flush — to sam fakt synchronizacji, to flush mode — to polityka, która odpowiada na pytanie: „A kiedy ty, Hibernate, w ogóle masz prawo lub obowiązek zrobić flush automatycznie?" To jak ustawienia autozapisu w edytorze tekstu: można zapisywać co N minut, można tylko przy zamykaniu, można wyłączyć autozapis i wciskać Ctrl+S ręcznie, a można ustawić tryb „zapisuj po każdym kichnięciu" — i szybko znienawidzić życie.
Ważny niuans: flush mode nie anuluje dirty checking i nie zmienia stanu encji. Twój Product jak był managed i „brudny", tak nim zostanie. Zmienia się tylko kiedy Hibernate zdecyduje „przetłumaczyć brud na SQL" automatycznie.
flowchart TD
A[Zmieniłeś managed-entity] --> B[dirty checking zaznacza zmiany]
B --> C{"flush mode decyduje kiedy flush jest dozwolony/wymagany"}
C --> D[flush wysyła SQL]
D --> E[commit/rollback kończy transakcję]
2. Tryby JPA i Hibernate
W świecie JPA pracujesz z jakarta.persistence.FlushModeType, i tam są tylko dwa tryby: AUTO i COMMIT. W świecie Hibernate jest org.hibernate.FlushMode, gdzie trybów jest więcej: AUTO, COMMIT, MANUAL, ALWAYS.
| Poziom API | Enum | Dostępne tryby | Co to oznacza w dwóch słowach |
|---|---|---|---|
| JPA | FlushModeType | AUTO, COMMIT | Standardowe zachowanie i „odłóż do commitu" |
| Hibernate | FlushMode | AUTO, COMMIT, MANUAL, ALWAYS | To samo + „tylko ręcznie" + „przed każdym zapytaniem" |
3. AUTO: tryb domyślny
AUTO — podstawowa polityka. Hibernate robi flush przed commit i może zrobić go wcześniej, jeśli inaczej zapytanie wewnątrz transakcji zobaczyłoby przestarzały stan względem już nagromadzonych zmian. Sens AUTO nie w tym, żeby „pisać częściej", lecz żeby nie kłamać zapytaniom wewnątrz bieżącego unit of work.
4. COMMIT: flush na końcu transakcji
Tryb COMMIT brzmi jak dobra optymalizacja: „bez wczesnych UPDATE, zrób wszystko na końcu". I co do zasady tak jest: Hibernate stara się nie robić automatycznego flush przed zapytaniami i odkłada synchronizację do momentu commit. Ale zapłata jest oczywista: zapytania wewnątrz transakcji mogą działać tak, jakby twoich zmian „jeszcze nie było".
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
entityManager.setFlushMode(FlushModeType.COMMIT);
Product p = entityManager.find(Product.class, 1L); // managed-encja
p.setName("Phone Pro (commit)");
Long count = entityManager.createQuery(
"select count(p) from Product p where p.name = :name", Long.class)
.setParameter("name", "Phone Pro (commit)")
.getSingleResult();
System.out.println("count = " + count); // często będzie 0, bo UPDATE jeszcze nie poszedł
5. MANUAL: flush tylko ręcznie
Tryb MANUAL — to gdy wyłączasz autozapis i mówisz Hibernate: „Nic sam nie wysyłasz. Chcesz SQL — zadzwonię". To tryb specyficzny dla Hibernate, nie ma go w JPA. Najuczciwszy ze wszystkich: bez jawnego flush() SQL nie pójdzie.
import jakarta.persistence.EntityManager;
import org.hibernate.FlushMode;
import org.hibernate.Session;
Session session = entityManager.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.MANUAL); // wyłączamy auto-flush
Product p = entityManager.find(Product.class, 1L);
p.setName("Phone Pro (manual)");
// W MANUAL nie liczymy ani na flush przed zapytaniami, ani na flush przy commit.
// Jeśli potrzebna synchronizacja — wywołujemy flush() jawnie.
session.flush();
6. ALWAYS: flush przed każdym zapytaniem
ALWAYS — to odwrotność COMMIT. Przed dowolnym zapytaniem — upewnij się, że baza jest zsynchronizowana z kontekstem. Na poziomie poprawności — tak. Na poziomie wydajności i nieoczekiwanych efektów ubocznych — czasem katastrofa.
import jakarta.persistence.EntityManager;
import org.hibernate.FlushMode;
import org.hibernate.Session;
Session session = entityManager.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.ALWAYS); // flush przed KAŻDYM zapytaniem
Product p = entityManager.find(Product.class, 1L);
p.setName("Phone Pro (always)");
entityManager.createQuery("select c from Customer c", Customer.class)
.getResultList(); // w ALWAYS flush nastąpi przed tym zapytaniem, nawet jeśli dotyczy Customer
7. Konfiguracja flush mode w kodzie
Najczęstszy błąd inżynierski z flush mode — nie „pomylić AUTO i COMMIT", lecz ustawiać tryb zbyt szeroko. Dlatego rozumienie, gdzie można go zadawać i jaki ma „promień rażenia" — ważne.
Wygodne podejście — ustawianie trybu flush na konkretnym zapytaniu, żeby nie zmieniać polityki całego kontekstu:
import jakarta.persistence.FlushModeType;
import java.util.List;
List<Product> products = entityManager.createQuery(
"select p from Product p", Product.class)
.setFlushMode(FlushModeType.COMMIT)
.getResultList();
8. Przykład: jedna zmiana i count
| Tryb | Flush przed zapytaniem count | Co zobaczy count | Główne odczucie |
|---|---|---|---|
| AUTO | Zwykle tak | „Jakby zmiany są już w bazie" | Poprawnie, ale czasem nieoczekiwanie wcześnie |
| COMMIT | Zwykle nie | „Jakby zmian jeszcze nie ma" | Ciszej, ale możliwe dziwne wyniki pośrednie |
| MANUAL | Nie (tylko ręcznie) | „Jakby zmian nie ma, dopóki nie flush()" | Maksymalna kontrola, maksymalna odpowiedzialność |
| ALWAYS | Tak | „Zawsze świeże" | Kosztowne i może irytować liczbą flush |
9. Typowe błędy przy pracy z flush modes
Błąd nr 1: postrzeganie COMMIT jako „bezpiecznego przyspieszenia".
Jeśli wewnątrz transakcji są zapytania agregujące, możesz dostać wyniki sprzeczne z tym, co właśnie zrobiłeś w kodzie.
Błąd nr 2: włączenie MANUAL i zapomnienie, że flush nie następuje już sam.
MANUAL łatwo zamienia zmiany w „brudnopis", który takim i pozostanie. Obiekty wyglądają poprawnie w pamięci, a baza żyje własnym życiem.
Błąd nr 3: oczekiwanie, że flush mode zmienia dirty checking lub stan encji.
Flush mode — to nie „tryb zapisu encji". Nie robi encji detached, nie zamienia managed w transient i nie anuluje snapshotów. Jeśli encja jest managed i zmieniłeś pole, dirty checking i tak to zauważy.
Błąd nr 4: włączenie ALWAYS i dziwienie się, że „nawet odczyt" prowokuje zapisy.
ALWAYS robi flush przed każdym zapytaniem. Czytasz Customer, a aktualizuje się Product. Nie hałas — to bardzo restrykcyjny tryb, który sam włączyłeś.
Błąd nr 5: zmienianie flush mode zbyt szeroko i na zbyt długo.
Gdy tryb jest ustawiany na poziomie EntityManager/Session i zapominasz przywrócić poprzednią wartość, zachowanie aplikacji staje się „zależne od historii wykonania kodu". Dziś ta metoda wywołana pierwsza — wszystko OK, jutro druga — SQL idzie inaczej.
GO TO FULL VERSION