1. Sens stanów encji
Gdy nowicjusz po raz pierwszy pisze kod JPA, zazwyczaj myśli tak: „Mam obiekt. Zmieniłem pole. Czyli baza powinna się zaktualizować". Brzmi to logicznie… dopóki nie dowiesz się, że JPA nie jest telepatą. Żeby ORM „zobaczył" zmiany i powiązał je z konkretnym wierszem w tabeli, obiekt musi być w prawidłowym stanie względem JPA. Dlatego dziś wprowadzamy prostą, ale potężną ideę: encja żyje w różnych stanach, i te stany wyjaśniają prawie wszystkie wczesne „dlaczego ono się tak zachowuje?!".
Ważne jest, żebyś złapał właściwą intuicję: JPA nie śledzi każdego obiektu JVM, który kiedykolwiek stworzyłeś. Śledzi tylko te obiekty, które w danym momencie są u niego „na ewidencji". A „ewidencja" tutaj to nie księgowość (choć czasem właśnie tak się to odczuwa), ale techniczny fakt: czy JPA widzi ten obiekt i czy kojarzy go z rekordem w bazie.
2. Tożsamość encji
Nie po prostu „worek pól"
Jeśli uprościć do jednego zdania, encja — to obiekt Java, który reprezentuje konkretny rekord w bazie danych (lub potencjalny rekord, jeśli nie jest jeszcze zapisany). I tu zaczyna się to, co wielu czuje intuicyjnie, ale rzadko formułuje: obiekt w Java i wiersz w bazie mają różną „naturę tożsamości".
W Java domyślnie tożsamość obiektu to, z grubsza mówiąc, „czy to ten sam egzemplarz w pamięci" (porównanie po referencji, ==). W bazie danych tożsamość wiersza to „czy to ten sam rekord" (zazwyczaj przez PRIMARY KEY). W JPA te światy się spotykają: chcemy myśleć obiektami, ale baza i tak żyje wierszami i kluczami.
Gdy tworzysz nowy Product przez new Product(), dostajesz obiekt z polami, ale bez tożsamości w bazie danych: albo nie ma id, albo nie jest ono jeszcze przypisane. Taki obiekt nie jest jeszcze powiązany z konkretnym wierszem tabeli product, bo wiersza na razie nie ma. I właśnie ten stan „obiekt nie ma jeszcze życia w bazie" jest centralną częścią dzisiejszego wykładu.
Dla naszego projektu shop-data-jpa wygląda to maksymalnie przyziemnie: sku — to biznesowy sens towaru (w rzeczywistości jest unikalny), ale techniczna tożsamość w bazie jest zazwyczaj wyrażona przez id. I JPA orientuje się właśnie na id, gdy „rozpoznaje" encję. Klucze biznesowe (jak sku) na pewno omówimy szerzej później, gdy będziemy mówić o equals() i hashCode(), ale już teraz warto pamiętać: encja ma „techniczną" tożsamość (PK) i „znaczeniową" (klucz biznesowy).
Mini-kawałek naszej encji (dla orientacji, nie pełny plik):
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity // Mówimy JPA, że ta klasa to encja (mapuje się na tabelę)
public class Product {
@Id // Techniczna tożsamość encji: po tym polu JPA "rozpoznaje" rekord
private Long id;
@Column(nullable = false, unique = true) // Klucz biznesowy: ważny dla sensu, ale nie równy "zarządzaniu przez JPA"
private String sku;
}
Nawet w tym małym fragmencie widać: sku jest ważny dla biznesu, ale JPA zaczyna „rozmowę" o encji właśnie od @Id.
3. Podstawowe stany encji
transient: obiekt poza JPA
Stan transient najłatwiej zrozumieć po ludzku: to zwykły nowy obiekt Java, stworzony przez new, który na razie nie jest częścią modelu persistence. Żyje w pamięci, może być ładny, wypełniony i nawet bardzo pewny siebie… ale dla bazy danych nie istnieje. To jak wypełnić ankietę, ale nie kliknąć przycisku „Wyślij": dane są, a w systemie ich nie ma.
Najczęstszy błąd na tym etapie — oczekiwać od obiektu transient zachowania „jak u zapisanej encji". Na przykład czekać, że ma już id, albo myśleć, że jeśli zmieniłeś pole, to JPA „jakoś to zobaczy". Obiekt transient nie ma żadnego połączenia z bazą. Może stać się encją w pełnym sensie tego słowa dopiero po tym, jak uczynimy go znanym dla JPA.
Mini-przykład obiektu transient w naszej domenie:
import com.example.shopdatajpa.catalog.entity.Product;
import java.math.BigDecimal;
Product product = new Product(); // transient: po prostu obiekt, JPA o nim nie wie
product.setSku("PHONE-1"); // zmiany tylko w pamięci JVM
product.setName("Phone");
product.setPrice(new BigDecimal("499.00"));
Tu product już istnieje jako obiekt, ale na razie to po prostu obiekt. Nawet jeśli wygląda „jak towar", w bazie jeszcze się nie pojawił. I najważniejsze: JPA na razie nie musi (i nie może) śledzić jego zmian, bo nie powiedziałeś jej jeszcze: „ta rzecz jest częścią modelu persistence".
managed: pod opieką JPA
Stan managed — to moment, gdy JPA mówi: „Okej, znam ten obiekt. Jest mój. Będę traktować go jako część bieżącej pracy z bazą". Encja managed zyskuje bardzo ważną właściwość: JPA może śledzić zmiany pól (właśnie „może", bez szczegółów „kiedy i jak poleci SQL" — to będzie później, teraz potrzebujemy intuicji). Dlatego ten sam wywołany setName(...) nagle zaczyna mieć inny sens: to już nie tylko zmiana w pamięci, to zmiana, którą JPA potencjalnie uwzględnia w ramach pracy z bazą.
Jak encja staje się managed? Na podstawowym poziomie są dwie zrozumiałe drogi. Pierwsza droga — załadowałeś encję z bazy przez JPA, i automatycznie staje się managed. Druga droga — poprosiłeś JPA o zapisanie nowego obiektu, i on też staje się managed, bo JPA musi „prowadzić" go jako część operacji.
Najprostszy sposób, żeby poczuć stan managed — przez EntityManager.find(...):
import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;
Product product = entityManager.find(Product.class, 1L); // managed: encja załadowana i śledzona
product.setName("Updated name"); // teraz JPA widzi zmianę (encja managed)
Kluczowa myśl: find() nie tylko zwraca obiekt. Zwraca obiekt, który JPA uważa za „podkontrolny" w ramach bieżącej pracy. Jeśli bardzo z grubsza, EntityManager — to „centrum kierowania lotami" dla encji: przez niego encje trafiają w strefę widoczności JPA.
Jeśli chcemy przykład z „narodzinami" managed z transient (idea, bez próby zrobienia teraz pełnoprawnego kodu produkcyjnego), wygląda to tak:
import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;
import java.math.BigDecimal;
Product product = new Product(); // transient: na razie po prostu obiekt
product.setSku("PHONE-1");
product.setName("Phone");
product.setPrice(new BigDecimal("499.00"));
entityManager.persist(product); // managed: JPA zaczyna zarządzać encją
// Ważne: SQL INSERT może wykonać się nie od razu tutaj (zależy od flush/transakcji) — to osobny temat
Sens persist() w kontekście dzisiejszego wykładu jest prosty: JPA teraz zna obiekt i trzyma go jako część modelu persistence. Nie omawiamy, kiedy dokładnie zostanie wykonany SQL INSERT i jak dokładnie wiąże się to z transakcją i synchronizacją — to osobny duży temat, który metodycznie pojawi się później. Teraz interesuje nas właśnie przejście: „był po prostu obiekt → stał się encją zarządzaną".
detached: obiekt bez śledzenia
detached — to stan, który najczęściej łamie oczekiwania, bo „w odczuciu" wszystko wygląda prawie tak samo jak w managed: obiekt masz w zmiennej, pola dostępne, gettery i settery działają, żaden błąd akurat teraz się nie pojawia. Ale jest jedna zasadnicza różnica: JPA już nie śledzi zmian tego obiektu. To znaczy możesz zmieniać pola ile chcesz, a dla bazy danych nie będzie to miało żadnego efektu — bo JPA o tym nie wie (i nie musi wiedzieć).
Jak encja staje się detached? Koncepcyjnie tak: była znana JPA (na przykład załadowana z bazy), ale potem „wyszła ze strefy widoczności" bieżącej pracy JPA. Może to się dziać z różnych powodów, ale teraz ważne jest, żebyśmy uchwycili podstawowy fakt: detached — to „była managed". Była już częścią modelu persistence, ale teraz stała się zwykłym obiektem bez automatycznego nadzoru ze strony ORM.
Z punktu widzenia EntityManager można dosłownie „odczepić" encję:
import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;
Product product = entityManager.find(Product.class, 1L); // managed: JPA śledzi encję
entityManager.detach(product); // detached: encję "odczepiono" od kontekstu
product.setName("Changed later"); // JPA już nie śledzi tej zmiany
Bardzo ważne, żeby nie mylić transient i detached. transient — to „jeszcze nigdy nie był częścią bazy". detached — to „już był częścią bazy, ale teraz nie jest pod kontrolą JPA".
I tak, tutaj łatwo złapać filozoficzną myśl: obiekt detached wciąż może mieć id, bo był zapisany wcześniej. Ale obecność id nie czyni go automatycznie managed. id — to oznaka tego, że obiekt może odpowiadać wierszowi w bazie, ale zarządzanie tym odpowiadaniem to osobna historia.
removed: oznaczone do usunięcia
Stan removed — to jeszcze jedna pułapka dla nowicjusza. Często wydaje się, że usunąć encję to „no skoro już nie chcę tego obiektu, to powinien zniknąć". I tu chce się napisać coś w stylu product = null; i poczuć się zwycięzcą. Ale null oznacza tylko: „nie mam już referencji w tej zmiennej". To w ogóle nie jest rozmowa z bazą danych. Baza nawet nie ma pojęcia, że przeżyłeś tam mały dramat zerwania relacji z obiektem.
removed w JPA oznacza co innego: encja jest oznaczona do usunięcia jako część bieżącej pracy z bazą. Ważne jest właśnie słowo „oznaczona": to stan wewnątrz mechaniki ORM, a nie natychmiastowe zniknięcie wszystkiego na świecie.
Mini-przykład:
import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;
Product product = entityManager.find(Product.class, 1L); // managed
entityManager.remove(product); // removed: JPA oznaczyła rekord do usunięcia
System.out.println(product.getSku()); // obiekt w pamięci żyje i jest dostępny (to nie GC i nie null)
Tu można zauważyć subtelną „różnicę inżynierską": usunięcie — to nie GC i nie „wymazać obiekt z RAM-u". To zamiar w ramach operacji persistence: „w bazie ten rekord trzeba usunąć". A obiekt jako obiekt Java może istnieć dalej — po prostu JPA już traktuje go inaczej.
I znów ten sam motyw dnia: identyczne z wyglądu działania (set..., czytanie pól, przypisanie zmiennej) mają różne konsekwencje w zależności od stanu encji.
4. Mapa stanów
Gdy terminów robi się więcej niż dwa, mózg zaczyna prosić: „Można poproszę mapę terenu? Nie jestem jeszcze gotowy być kompasem". To normalna reakcja. Dlatego teraz zbierzemy wszystko w jeden kompaktowy „atlas": co oznacza każdy stan, jak do niego trafić i czego oczekiwać od JPA.
W postaci tabeli wygląda to tak:
| Stan | Co to po ludzku | Skąd się bierze | JPA śledzi zmiany? | Jest połączenie z bazą? |
|---|---|---|---|---|
| transient | nowy obiekt w pamięci | new Product() | nie | nie (rekordu może nie być) |
| managed | obiekt „pod kontrolą" JPA | find(...), persist(...) | tak | tak (lub zostanie utworzone) |
| detached | obiekt „odczepił się" od JPA | detach(...) lub wyjście z kontekstu | nie | tak (zazwyczaj rekord już istnieje) |
| removed | obiekt oznaczony do usunięcia | remove(...) | JPA prowadzi go jako usuwany | tak (rekord będzie usuwany) |
Jeśli chcemy zobaczyć to jako „przejścia stanów", można wyobrazić sobie taki schemat. Nie próbuje być encyklopedią całego JPA (świadomie trzymamy minimum), ale doskonale utrwala sens:
stateDiagram-v2
%% Podstawowa "mapa" przejść: to nie pełna encyklopedia JPA, ale minimalna ściągawka
[*] --> Transient: "new"
Transient --> Managed: "persist(...)"
Managed --> Detached: "detach(...)"
Managed --> Removed: "remove(...)"
Zwróć uwagę, że nie ma tu niektórych metod i stanów, które spotkasz później w kursie (i to normalne). Dziś ważne jest, żebyś pewnie rozróżniał podstawową czwórkę i nie myślał, że „encja — to po prostu obiekt". W naszym projekcie shop-data-jpa ta wiedza już teraz pomoże ci trzeźwo oceniać, dlaczego w jednym miejscu zmiany „są podchwytywane", a w innym — nie, nawet jeśli piszesz identyczne settery.
Jeśli chcesz całkiem krótką regułę na pamięć, brzmi ona tak: managed — to „JPA widzi", detached/transient — „JPA nie widzi", removed — „JPA widzi i zamierza usunąć". Stany — to nie „teoria dla teorii", to język, w którym wyjaśniasz zachowanie modelu persistence.
5. Typowe błędy przy pracy ze stanami encji
Błąd nr 1: uważać, że każdy Product automatycznie „żyje w bazie", skoro na klasie jest @Entity.
Adnotacja czyni klasę encją z punktu widzenia mapowania, ale nie czyni każdego egzemplarza tej klasy zarządzanym. new Product() — to po prostu obiekt. Dopóki nie załadowałeś go przez JPA lub nie uczyniłeś go „znanym" dla ORM, pozostaje transient.
Błąd nr 2: mylić transient i detached, bo „i tu, i tam JPA nie śledzi".
To naprawdę podobne odczucia, ale sens jest różny. transient — „jeszcze nigdy nie był częścią modelu persistence", zazwyczaj bez id. detached — „już był, ale teraz się odczepił", często z id. To rozróżnienie potem będzie krytyczne, gdy zaczniesz aktualizować dane i dziwić się, dlaczego jedno się aktualizuje, a inne nie.
Błąd nr 3: oczekiwać, że wywołanie setName(...) samo w sobie „aktualizuje bazę", niezależnie od stanu.
Setter zmienia pole obiektu, i to zawsze działa na poziomie Java. Ale efekt dla bazy zależy od tego, czy obiekt jest managed, czy nie. Jeśli obiekt jest detached, możesz choćby dziesięć razy wywołać setName(...), baza nie „musi" się o tym dowiedzieć. Dla bazy po prostu zmieniłeś pole u obiektu w pamięci — i tyle.
Błąd nr 4: myśleć, że removed — to „obiekt zniknął" i po remove() nie można się do niego odwoływać.
removed — to o zamiarze usunięcia rekordu w bazie, a nie o zniszczeniu obiektu w pamięci. Referencja Java pozostaje referencją, obiekt pozostaje obiektem, a jego pola są dostępne. Po prostu teraz JPA uważa ten obiekt za oznaczony do usunięcia, a nie za zwykłą „żywą" encję.
Błąd nr 5: podmieniać usunięcie z bazy usunięciem referencji (product = null;).
null wpływa tylko na twoją zmienną. Baza danych nie czyta twoich myśli i nie zagląda do twojego heapa (i całe szczęście). Jeśli trzeba usunąć wiersz w tabeli, robi się to przez operację usunięcia JPA, a nie przez „wyzerowanie".
GO TO FULL VERSION