CodeGym /Kursy /Hibernate deep-dive /persist() i odłożony INSERT

persist() i odłożony INSERT

Hibernate deep-dive
Poziom 2, Lekcja 1
Dostępny

1. Nieporozumienie wokół persist()

persist() nie jest równoznaczny z natychmiastowym INSERT. Jeśli pisałeś kiedyś kod w stylu „utwórz obiekt — zapisz do bazy", to z pewnością miałeś w głowie prosty i bardzo ludzki obraz: wywołałeś zapis — i gdzieś w PostgreSQL od razu pojawił się INSERT. Model przyjemny jak przycisk „Zapisz" w Wordzie: nacisnąłeś — zapisało się. Ale Hibernate działa trochę inaczej i właśnie stąd biorą się późniejsze niespodzianki w logu SQL.

Nieporozumienie zwykle rodzi się z trzech źródeł. Po pierwsze, wielu widziało save() w Spring Data i nieświadomie utożsamia każdą operację zapisu z natychmiastowym trafieniem do bazy. Po drugie, myli nas sam kod: linia entityManager.persist(product) wygląda jak „no to zapisz!". Po trzecie, SQL wykonuje się nie w momencie „nacisnąłem przycisk", lecz w momencie synchronizacji kontekstu z bazą — i to łamie intuicję.

Najbardziej uczciwa teza tego wykładu brzmi tak: persist() to przede wszystkim operacja na pamięci aplikacji (persistence context), a nie na tabeli w bazie. Baza oczywiście ostatecznie się do tego przyłączy... ale trochę później.

Mapa stanów jest już dostępna; teraz potrzebujemy pierwszego konkretnego przejścia. Nowy obiekt musi przestać być zwykłym new Product() i trafić pod zarządzanie Hibernate — i dokładnie to robi persist().

2. persist() jako rejestracja encji

Najłatwiej rozumieć persist() jako rejestrację nowej encji w persistence context. Wyobraź sobie, że to nie „magia Hibernate", lecz całkiem przyziemna lista obiektów, które ORM teraz śledzi w ramach bieżącej pracy. Wtedy persist() to polecenie: „Hej, Hibernate, oto ten obiekt — nowy. Weź go pod zarządzanie". Dopiero po tym obiekt staje się prawdziwą encją ORM, a nie zwykłą klasą Java z polami.

Spójrzmy na minimalny przykład. Przed persist() obiekt jest zwykłym transient: stworzyłeś go przez new, wypełniłeś pola, ale ORM nic o nim nie wie.

Product product = new Product();     // nowy obiekt: ORM jeszcze o nim nie wie
product.setSku("SKU-101");           // wypełniamy pola w pamięci aplikacji
product.setName("Mouse");

// product: transient (jeszcze nie w persistence context)
entityManager.persist(product);      // rejestrujemy obiekt w persistence context
// product: managed (ale to jeszcze nie gwarantuje INSERT właśnie teraz)

Zwróć uwagę na ważny niuans: uważamy już obiekt za managed od razu po persist(), nawet jeśli wstawienie SQL do tabeli jeszcze nie nastąpiło. Stan jest określany relacją do kontekstu, a nie tym, co już widziałeś w PostgreSQL.

Najłatwiej sprawdzić to przejście przez contains(): potrzebujemy tylko odpowiedzi, czy konkretna instancja jest powiązana z bieżącym EntityManager.

// Przed persist() obiekt nie jest pod zarządzaniem bieżącego persistence context
System.out.println(entityManager.contains(product)); // false

entityManager.persist(product); // po tym obiekt staje się managed

// Po persist() obiekt jest już w persistence context (nawet jeśli INSERT jeszcze nie był wykonany)
System.out.println(entityManager.contains(product)); // true

Jeśli contains() zwrócił true, obiekt jest już w persistence context. Na potrzeby tego wykładu tyle wystarczy: persist() zadziałał jako przejście transient -> managed, nawet jeśli SQL wciąż czeka na synchronizację.

3. Odłożony INSERT i unit of work

Teraz dochodzimy do najważniejszej i najczęściej łamiącej intuicję części: INSERT po persist() jest często odkładany. Hibernate (i JPA ogólnie) nie pracuje w trybie „jedna linia kodu — jedna komenda SQL", lecz w modelu unit of work: najpierw gromadzi zmiany, potem synchronizuje je z bazą.

Wewnątrz unit of work Hibernate robi dwie główne rzeczy. Najpierw zbiera zmiany: nowe encje, zmienione, usuwane. Potem, gdy nadchodzi moment synchronizacji, wysyła odpowiednie INSERT/UPDATE/DELETE do bazy. Pozwala mu to porządkować operacje, uwzględniać zależności, nie jeździć JDBC tam i z powrotem przy każdej drobiazgu i trzymać spójny obraz całości.

Bardzo uproszczony schemat tego, co dzieje się z nową encją:

flowchart TD
    A["Java: new Product()"] --> B[transient]
    B -->|"persist()"| C["managed w persistence context"]
    C --> D["plan INSERT wewnątrz unit of work"]
    D -->|synchronizacja kontekstu z bazą| E["SQL INSERT trafia do PostgreSQL"]

Kluczowa myśl: po persist() Hibernate ma „plan wstawienia", ale samo wstawienie może nastąpić później. Najczęściej — bliżej zakończenia transakcji. Czasem wcześniej, jeśli system musi się zsynchronizować — na przykład żeby prawidłowo wykonać zapytanie zależne od tych danych. Ale dokładne „kiedy" określa faza flush(). Tu wystarczy nam słownik: persist() planuje, a „wstawić" to osobna faza.

4. id: kiedy się pojawia

Pojawienie się id nie oznacza, że rekord już leży w bazie. Logika „jeśli id się pojawił, to znaczy wszystko zapisane" brzmi naturalnie, ale w Hibernate nie zawsze jest to poprawny wniosek.

Żeby zobaczyć, dlaczego id nie można używać jako wskaźnika zapisu, wygodnie wziąć oddzielny uproszczony mapping z SEQUENCE. Tu ważna nie jest konkretna encja projektu, lecz sam princip: moment pojawienia się id zależy od strategii generowania.

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
class Product {
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE) // ID może być pobrany z sekwencji przed INSERT
  private Long id; // to pole wypełniają "wewnętrzne" mechanizmy ORM/bazy
}

Dlaczego ważne jest to właśnie w wykładzie o persist()? Ponieważ różne strategie generowania id zachowują się różnie. Czasem Hibernate może pobrać id przed właściwym INSERT — na przykład przez sekwencję. Czasem id pojawia się dopiero po wstawieniu — na przykład przy podejściu identity. W obu przypadkach sens persist() jest taki sam: obiekt stał się managed. Ale twoje obserwacje w kodzie będą różne, i to często dezorientuje nowicjuszy.

Ważna zasada: nie używaj id jako uniwersalnego wskaźnika „zapisano do bazy".

5. Jak zobaczyć właściwy INSERT: SQL trace i flush()

Gdy mówimy „wstawienie nastąpi później", u studenta zwykle pojawia się słuszne pytanie: „Okej, a jak mogę to zobaczyć?" Właściwa odpowiedź kursu — przez obserwowalność: SQL trace. Po to właśnie włączyliśmy logowanie SQL. Hibernate nie jest telepatą: w końcu i tak rozmawia z bazą przez SQL, i właśnie to trzeba czytać.

entityManager.persist(product); // rejestrujemy encję (INSERT może jeszcze nie być wykonany)

System.out.println("id after persist = " + product.getId());

entityManager.flush(); // wymuszona synchronizacja: tu Hibernate musi wysłać INSERT

System.out.println("id after flush = " + product.getId());
// po flush INSERT poszedł, id jest zsynchronizowane z rekordem

Jeśli masz włączony SQL trace, w logu zobaczysz mniej więcej taki obraz (uproszczony):

-- Tak ORM "pobiera" ID (sekwencja) i następnie wykonuje właściwe wstawienie
select nextval('product_seq');
insert into products (id, sku, name) values (?, ?, ?);

Ważna jest nie konkretna SQL, lecz obserwacja: między persist() a momentem faktycznego wstawienia jest odstęp, zarządzany przez fazę synchronizacji.

6. persist() w Commerce Persistence Lab

Teraz zakorzenijmy wszystko w naszym przekrojowym projekcie. Przypomnijmy kontekst: Commerce Persistence Lab to laboratoryjny backend, gdzie modelujemy katalog (Product), klientów (Customer) i zamówienia (PurchaseOrder). Teraz nie potrzebujemy ani warstwy REST, ani „prawdziwego sklepu internetowego"; potrzebujemy odtwarzalnego środowiska, gdzie widać, co dzieje się z encją w pamięci i jaki SQL naprawdę trafia do PostgreSQL.

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

@Transactional // granica unit of work: wewnątrz Hibernate gromadzi zmiany i potem synchronizuje z bazą
public Long createProduct(String sku, String name) {
  Product product = new Product(sku, name); // transient: obiekt na razie "żyje" tylko w pamięci
  entityManager.persist(product);           // managed: dodaliśmy do persistence context (INSERT będzie później)
  return product.getId();                   // ID może być już przypisane, ale to nie równa się "commit do bazy"
}

Tu ważne nie to, że metoda zwraca id (choć to wygodne), lecz to, że po persist() obiekt stał się częścią bieżącego unit of work. Będzie żył wewnątrz kontekstu do końca transakcji, a Hibernate będzie go uważał za „swój".

7. Późne błędy integralności przy persist()

Błędy integralności przy persist() często wychodzą później, a nie w momencie wywołania. Wywołujesz persist(), kod wesoło idzie dalej, a potem — bum — wyjątek. I patrzysz na stacktrace i myślisz: „Dlaczego padło nie na persist(), lecz gdzieś na końcu metody lub nawet przy wyjściu z niej?"

Przyczyna jest prawie zawsze jedna i ta sama: właściwy INSERT poleciał do bazy nie w momencie persist(), lecz później. A więc i sprawdzenie ograniczeń bazy — unikalności, NOT NULL, kluczy obcych — też następuje później.

Product a = new Product("SKU-777", "Mouse");
Product b = new Product("SKU-777", "Mouse 2");

entityManager.persist(a);
entityManager.persist(b);

entityManager.flush(); // wyjątek będzie tutaj, a nie na persist()

Co ważne: persist() nie musi „chodzić do bazy i sprawdzać na miejscu". Jego zadaniem jest umieścić encję w planie. Sprawdzanie ograniczeń — to zadanie bazy w momencie właściwego SQL. Dlatego w świecie ORM normalne jest, że błąd wychodzi później. I to nie powód, żeby złościć się na Hibernate; to powód, żeby pamiętać, że mamy dwie rzeczywistości: rzeczywistość kontekstu w pamięci i rzeczywistość tabel w bazie, synchronizowane nie co milisekundę.

8. Typowe błędy przy pracy z persist()

Błąd nr 1: myślenie, że persist() to INSERT „właśnie teraz".
To najbardziej podstawowa pułapka. Piszesz persist(), oczekujesz natychmiastowego zapisu, a potem dziwisz się, że w logu SQL cisza. Hibernate mógł po prostu jeszcze nie zsynchronizować kontekstu z bazą. Jeśli chcesz zobaczyć moment zapisu, patrz na SQL trace lub użyj flush() jako edukacyjnej „latarki".

Błąd nr 2: używanie id jako wskaźnika „już leży w tabeli".
id może pojawić się przed INSERT, po INSERT lub w ogóle być przypisany ręcznie. A transakcja może się wycofać. Dlatego id != null to użyteczny fakt, ale nie dowód, że wiersz już istnieje w bazie i przeżył commit.

Błąd nr 3: wywoływanie persist() dla obiektu, który nie jest nowy (transient).
persist() jest operacją „dla nowych" w dobrym sensie: wprowadza nowy obiekt do kontekstu. Jeśli obiekt już istnieje, był wcześniej zapisany lub trafił do ciebie jako detached, persist() może prowadzić do wyjątków.

Błąd nr 4: zapomnienie o transakcji i oczekiwanie normalnego zachowania.
Zapis to niemal zawsze historia transakcyjna. W aplikacji Spring oznacza to zwykle @Transactional na metodzie serwisowej. Jeśli wywołujesz persist() w dziwnym miejscu bez granicy transakcyjnej, ryzykujesz błędy „transaction required" lub zachowanie „niby persist był, ale danych nie ma".

Błąd nr 5: dziwienie się „późnym" wyjątkom (unique constraint, NOT NULL) i szukanie problemu nie tam.
Jeśli błąd bazy pojawił się na flush() lub przy wyjściu z transakcji — to logiczne: właśnie wtedy mógł polecieć właściwy INSERT. Nie trzeba tego postrzegać jako „Hibernate się zepsuł". To normalna cena odłożonej synchronizacji: problemy integralności pojawiają się wtedy, gdy SQL naprawdę trafia do PostgreSQL.

1
Zadanie
Hibernate deep-dive, poziom 2, lekcja 1
Niedostępne
`persist()` przenosi nowy obiekt do `managed`
`persist()` przenosi nowy obiekt do `managed`
1
Zadanie
Hibernate deep-dive, poziom 2, lekcja 1
Niedostępne
SQL-widocznosc nowego wiersza po `flush()`
SQL-widocznosc nowego wiersza po `flush()`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION