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.
GO TO FULL VERSION