CodeGym /Kursy /Hibernate deep-dive /Kolejność SQL w jednym flush

Kolejność SQL w jednym flush

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

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.

1
Zadanie
Hibernate deep-dive, poziom 4, lekcja 3
Niedostępne
Kolejność INSERT dla rodzica i wiersza dziecka
Kolejność INSERT dla rodzica i wiersza dziecka
1
Zadanie
Hibernate deep-dive, poziom 4, lekcja 3
Niedostępne
Kolejność DELETE przy usuwaniu rodzica i wiersza podrzędnego
Kolejność DELETE przy usuwaniu rodzica i wiersza podrzędnego
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION