1. Flush — to paczka zmian, a nie „jedna komenda SQL"
Jeśli dopiero zaczynasz czytać log SQL Hibernate, flush łatwo wyobrazić sobie jak jeden przycisk: nacisnąłeś — poleciała jedna komenda, jak git commit, tylko dla bazy. Niestety (lub na szczęście), flush to raczej „paczka roboty", którą Hibernate nagromadził, gdy spokojnie zmieniałeś obiekty w pamięci. A paczka ta może zawierać kilka INSERT, kilka UPDATE, kilka DELETE, a czasem jeszcze operacje na powiązaniach.
Hibernate po drodze transakcji składa zmiany w coś w rodzaju „kolejki działań". Wywołałeś persist(), zmieniłeś pole przez setter, wywołałeś remove(), a może nie wywołałeś niczego jawnie — i dirty checking sam oznaczył obiekt jako zmieniony. To wszystko zamienia się nie w natychmiastowy SQL, lecz w „plan": co wstawić, co zaktualizować, co usunąć. Gdy nadchodzi moment flush, Hibernate bierze ten plan i zaczyna go wykonywać.
Stąd pierwszy ważny wniosek wykładu: wewnątrz flush prawie zawsze jest wiele operacji SQL, i Hibernate będzie próbował je wykonać tak, żeby baza danych nie zaczęła krzyczeć o kluczach obcych, unikalności i innych regułach integralności. Czyli myśli nie „w jakiej kolejności napisałeś linie Javy", lecz „w jakiej kolejności baza zgodzi się przyjąć zmiany".
2. Kolejność Javy i SQL przy flush
Gdy programista po raz pierwszy widzi „nie tę kolejność SQL", często zaczyna podejrzewać, że Hibernate „przypadkowo pomylił". W rzeczywistości Hibernate po prostu robi to, co musi: synchronizuje stan obiektów z bazą w poprawnej kolejności. A poprawność tu jest wyznaczana nie stylem kodu, lecz regułami modelu relacyjnego.
Najczęstsza przyczyna — klucze obce. Druga — Hibernate grupuje operacje według typów: wstawki osobno, aktualizacje osobno, usunięcia osobno. Stąd główna reguła: kolejność SQL przy flush — to kolejność spełniania zależności, a nie kolejność twoich myśli.
3. Klucze obce i kolejność operacji
W naszym projekcie takich zależności jest wiele, bo domena „katalog → zamówienia → pozycje → stany magazynowe" jest naturalnie relacyjna. Na przykład, OrderItem odwołuje się do PurchaseOrder i do Product. PurchaseOrder odwołuje się do Customer.
flowchart LR Customer -->|FK customer_id| PurchaseOrder PurchaseOrder -->|FK order_id| OrderItem Product -->|FK product_id| OrderItem Product -->|FK product_id| InventoryItem
| Operacja | Czego chce baza danych | Typowa konsekwencja dla kolejności |
|---|---|---|
| INSERT dziecka z FK | „Referencja musi wskazywać na istniejący wiersz" | najpierw wstawiamy rodzica, potem dziecko |
| DELETE rodzica | „Nie można usunąć wiersza, do którego są referencje" | najpierw usuwamy dzieci, potem rodzica |
| UPDATE pola FK | „Nowa referencja musi być prawidłowa" | insert nowego rodzica (jeśli tworzymy) → update dziecka |
4. Kolejność INSERT przy flush
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void insertOrderWithItems(EntityManager em) {
// Tworzymy nowego "rodzica" (na razie tylko w pamięci)
PurchaseOrder order = new PurchaseOrder();
order.setOrderNumber("ORD-1001");
// Tworzymy "dziecko" i łączymy je z rodzicem (FK pojawi się w INSERT order_item)
OrderItem item = new OrderItem();
item.setOrder(order);
item.setQuantity(1);
// persist() tworzy plan wstawień, ale nie gwarantuje kolejności SQL jeden-do-jednego
em.persist(order);
em.persist(item);
// Przy flush Hibernate wyśle INSERT-y tak, żeby FK były prawidłowe
em.flush();
}
Typowy fragment SQL (pseudo-log):
-- flush begins
insert into purchase_order (order_number, ...) values ('ORD-1001', ...);
insert into order_item (order_id, quantity, ...) values (123, 1, ...);
-- flush ends
5. Kolejność DELETE przy flush
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void deleteOrder(EntityManager em) {
PurchaseOrder order = em.find(PurchaseOrder.class, 1L);
em.remove(order);
em.flush();
}
-- Najpierw usuwamy "dzieci", bo inaczej FK nie pozwoli usunąć rodzica
delete from order_item where order_id = 1;
-- Potem usuwamy "rodzica", gdy nikt już do niego nie odsyła
delete from purchase_order where id = 1;
6. Czytanie logu SQL wokół flush
Czytanie logu SQL to umiejętność, która na początku przypomina oglądanie filmu, gdzie przypadkowo włączyłeś tryb „pokaż scenariusz". Żeby nie tonąć, czytaj log jako epizody flush: znajdź punkt, gdzie flush na pewno nastąpił, i patrz na blok SQL, który poszedł „wokół" tej punktu.
| Co widzisz w kodzie | Co to oznacza | Jak sprawdzić w SQL |
|---|---|---|
| setName(...) u managed-encji | „możliwy UPDATE" | szukamy update ... set ... where id = ... przy flush |
| persist(...) | „przyszły INSERT" | szukamy insert into ... przy flush |
| remove(...) | „przyszły DELETE (możliwie kaskadowy)" | szukamy delete from ... i możliwe preparujące update |
7. Typowe błędy przy flush
Błąd nr 1: próba wyjaśniania SQL po kolejności linii Javy.
Mózg lubi liniowe historie. Hibernate przy flush myśli nie historią, lecz zależnościami i integralnością danych.
Błąd nr 2: nieuwzględnianie kluczy obcych i dziwienie się constraint violation przy usuwaniu.
Gdy wywołujesz remove(order), a baza krzyczy — problem nie w tym, że Hibernate „nie usunął pozycji zamówienia", lecz w tym, że nie pomyślałeś, jakie wiersze do czego się odwołują.
Błąd nr 3: patrzenie tylko na jeden UPDATE/DELETE w logu i ignorowanie całego bloku flush.
Flush — paczka. Jeśli wyciągnąłeś z środka logu jeden update product, łatwo pominąć, że nieco wyżej był insert order_item, a nieco niżej — delete customer_address.
Błąd nr 4: uważanie, że remove() oznacza natychmiastowy DELETE w bazie.
Remove() zmienia stan encji na removed w kontekście. Realny DELETE zwykle idzie przy flush.
Błąd nr 5: mylenie „co w bazie" i „co w persistence context".
Gdy po flush robisz find() tego samego obiektu bez clear(), możesz zobaczyć „nową wartość" nie dlatego, że przyszła z bazy, lecz dlatego, że widzisz nadal tę samą managed-kopię w pamięci.
GO TO FULL VERSION