1. Infrastruktura przed JPA
Zanim pojawią się encje, aplikacja musi mieć infrastrukturę, która fizycznie umie komunikować się z bazą danych. Jeśli jesteś początkujący, chęć „no to już piszemy @Entity i zapisujemy produkty" jest całkowicie zrozumiała. Ale bez tej infrastruktury budujemy dom z ładnymi tapetami i bez fundamentów. Trzyma się dokładnie do pierwszego wiatru, czyli do pierwszego błędu połączenia.
W tym wykładzie porządkujemy sobie w głowie: jakie dokładnie komponenty uruchamiają się w aplikacji Spring Boot, gdy dodajesz starter JPA. Naszym celem jest usunięcie poczucia „magii startera" i zastąpienie go normalnym inżynierskim obrazem: kto za co odpowiada, jak komponenty są ze sobą powiązane i jak przekonać się, że naprawdę istnieją w kontekście Spring.
Tu nie omawiamy samego URL, loginu i hasła. Teraz ważniejsze jest zrozumienie, co dokładnie pojawia się w aplikacji i dlaczego bez tego JPA nie wystartuje.
2. Skład spring-boot-starter-data-jpa
Gdy dodajesz zależność spring-boot-starter-data-jpa, nie po prostu „podłączasz bibliotekę do adnotacji @Entity". Mniej więcej mówisz Spring Boot: „Drogi Boot, chcę stos JPA do pracy z danymi, proszę zbuduj mi infrastrukturę". I Boot zaczyna to robić, opierając się na dwóch rzeczach: obecności potrzebnych klas w classpath i ustawieniach, które podamy w następnym wykładzie.
Przydatne jest od razu trzymanie w głowie dwóch sensów startera. Po pierwsze, podciąga właściwy zestaw zależności: JPA, ORM, Spring Data i transakcje. Po drugie, uruchamia łańcuch auto-konfiguracji, który tworzy beany jak DataSource, EntityManagerFactory i PlatformTransactionManager. To właśnie ta „magia", która jest w rzeczywistości całkiem przewidywalna, jeśli wiesz, co dokładnie powinno się pojawić.
Minimalny fragment zależności Gradle na dziś wygląda tak:
dependencies {
// JPA + Spring Data + transakcje + auto-konfiguracja infrastruktury
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// Sterownik potrzebny na etapie uruchamiania/działania aplikacji, ale zwykle nie do kompilacji
runtimeOnly("org.postgresql:postgresql")
}
Zwróć uwagę na słowo runtimeOnly przy sterowniku PostgreSQL. To nie „kaprys autora kursu" i nie estetyka. To dosłownie coś, bez czego Spring Boot może zbudować piękną infrastrukturę JPA — a potem i tak się wysypać, bo nie ma czym rozmawiać z PostgreSQL.
3. Komponenty stosu JPA
Poniżej będą krótkie fragmenty weryfikacyjne. To nie część stałego szkieletu projektu, lecz jednorazowe sprawdzenia: zobaczyli prawdziwy bean, zrozumieli jego rolę i usunęli kod. Tak łatwiej zdjąć poczucie magii i nie zamieniać main() w magazyn diagnostycznych println-ów.
Sterownik JDBC dla PostgreSQL
O sterowniku łatwo zapomnieć: w przykładach internetowych jest on „gdzieś tam już gotowy", i wszystko jakby działa samo. Ale jeśli wyjaśniać uczciwie, sterownik — to tłumacz między aplikacją a bazą danych. PostgreSQL nie rozumie obiektów Java, a Spring nie umie telepatycznie czytać tabel. Potrzebna jest warstwa, która mówi z bazą w jej protokole i implementuje standardowe API JDBC.
W twojej aplikacji sterownik zazwyczaj nie figuruje bezpośrednio w kodzie. Nie tworzysz new org.postgresql.Driver() (i chwała Bogu). Ale przy starcie aplikacji Spring Boot musi zdołać zrozumieć: „Okej, baza — PostgreSQL, znaczy sterownik taki-to". I dalej przez JDBC tworzone są prawdziwe połączenia, przez które wysyłane są polecenia SQL.
Przydatna sprawdzian: jeśli dodałeś spring-boot-starter-data-jpa, ale zapomniałeś sterownika PostgreSQL, aplikacja może przejść część startu i potem wysypać się z błędem „no suitable driver". W tym momencie warto nie kląć na ORM, lecz spokojnie powiedzieć sobie: „Okej, infrastrukturę JPA mam tylko w teorii, ale transportu do bazy nie ma".
Łańcuch znaczeń powinien ułożyć się w głowie tak: PostgreSQL (serwer) ← sterownik JDBC ← DataSource ← JPA. Bez środkowego ogniwa reszta zamienia się w filozofię.
DataSource: skąd biorą się połączenia
DataSource — jeden z głównych obiektów dzisiejszego dnia, bo odpowiada za prostą i surową rzecz: „daj mi połączenie z bazą". W świecie Javy podstawową jednostką komunikacji z bazą jest Connection. Tworzenie go jest drogie, mnożenia na każdy kaprys — nie można, zamykać — trzeba ostrożnie. Dlatego w normalnej aplikacji prawie nigdy nie tworzysz połączeń ręcznie: dostajesz je przez DataSource, a dalej infrastruktura zarządza ich cyklem życia.
Przydatna analogia (z lekką autoironią): baza danych — to restauracja, Connection — to stolik, a DataSource — to hostessa przy wejściu. Nie musisz wpadać do kuchni i szukać wolnego stolika samemu. Mówisz: „Poproszę stolik dla dwóch" — i albo cię sadzają, albo grzecznie informują „miejsc nie ma" (i to też ważna informacja).
Ważne jest rozróżnienie kilku podobnych słów, bo właśnie na nich początkujący się potykają:
| Termin | Co to jest | Przybliżony „czas życia" |
|---|---|---|
| PostgreSQL | proces/serwer bazy danych (w naszym przypadku — kontener) | dopóki działa docker compose up |
| sterownik JDBC | biblioteka w aplikacji | tyle samo, co żyje aplikacja |
| DataSource | fabryka/dostawca połączeń | zwykle jeden bean na całą aplikację |
| Connection | konkretne połączenie | krótko: „na czas pracy z bazą" (często — wewnątrz transakcji) |
Najfajniejsze: Spring Boot po dodaniu stosu JPA i poprawnych ustawieniach tworzy DataSource automatycznie, i możesz go dosłownie zobaczyć w kontekście. Poniżej — właśnie jednorazowa weryfikacja, żeby zdjąć poczucie magii. Trzymać takiego kodu w main() jako stały szkielet projektu — nie ma sensu.
import javax.sql.DataSource;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.PlatformTransactionManager;
@SpringBootApplication
public class ShopDataJpaApplication {
public static void main(String[] args) {
// Jednorazowo podnosimy kontekst Spring, żeby zobaczyć,
// jakie infrastrukturalne beany naprawdę się stworzyły
var context = SpringApplication.run(ShopDataJpaApplication.class, args);
// Patrzymy, jaki DataSource się podniósł (zazwyczaj to HikariCP)
System.out.println(context.getBean(DataSource.class).getClass().getName());
// com.zaxxer.hikari.HikariDataSource
// Patrzymy, jaki menedżer transakcji jest używany dla JPA
System.out.println(context.getBean(PlatformTransactionManager.class).getClass().getName());
// org.springframework.orm.jpa.JpaTransactionManager
}
}
Zobaczyli prawdziwe klasy beanów — i to wystarczy. Sens nie polega na tym, żeby na zawsze osiedlić System.out.println w punkcie wejścia, lecz na tym, żeby raz zobaczyć: infrastruktura naprawdę się podniosła.
Tak, po drodze zobaczyliśmy już Hikari i JpaTransactionManager. Ale nie trzeba się bać: w tym wykładzie nie „studiujemy HikariCP", po prostu odnotowujemy fakt — to są prawdziwe obiekty, a nie bajkowe stworzenia z lasu auto-konfiguracji.
Jeszcze jeden mały, ale pokazowy fragment: jeśli DataSource się podniósł, możesz pobrać Connection i sprawdzić, do czego jest naprawdę podłączony. To taka sama tymczasowa niskopoziomowa weryfikacja, a nie stały styl punktu wejścia.
import java.sql.Connection;
import javax.sql.DataSource;
public class DbSmoke {
public static void ping(DataSource dataSource) throws Exception {
// Ważne: połączenie musimy zamknąć obowiązkowo.
// Dla puli oznacza to "zwróć do puli", a nie "zniszcz fizyczne połączenie".
try (Connection c = dataSource.getConnection()) {
// Szybka weryfikacja: jaki URL naprawdę używa JDBC
System.out.println(c.getMetaData().getURL()); // jdbc:postgresql://localhost:5432/shop
// Jeszcze jeden prosty smoke-check: czy połączenie jest żywe?
System.out.println(c.isValid(2)); // true
}
}
}
Tu ważne, że połączenie jest zamykane przez try-with-resources. Nawet jeśli połączenia faktycznie pochodzą z puli, i tak trzeba je zamykać: dla puli oznacza to „zwróć połączenie z powrotem", a nie „zniszcz je fizycznie". Ale to już płynny mostek do następnego wykładu o puli.
EntityManagerFactory: skąd bierze się EntityManager
Po DataSource łatwo zdecydować: „no dobrze, podłączyłem się do bazy". I to prawda — ale tylko na poziomie JDBC. JPA — to następne piętro. W tym świecie podstawowym obiektem roboczym jest EntityManager. Pozwala pracować z encjami i zapytaniami nie jak z „surowym SQL w stringach", lecz przez model ORM.
Ale EntityManager nie tworzy się sam. Tworzy go fabryka — EntityManagerFactory. I to ważne ułożyć w głowie z wyprzedzeniem, żeby później nie mylić poziomów infrastruktury.
Grubsza analogia, bez wchodzenia we wnętrzności: EntityManagerFactory — to ciężki obiekt, który tworzy się przy starcie aplikacji i żyje długo. A EntityManager — to bardziej „krótkożyjący" obiekt, używany wewnątrz operacji pracy z danymi, często wewnątrz transakcji. Fabryka istnieje po to, żeby nie budować całego świata JPA od nowa dla każdego żądania.
Sprawdzić, że EntityManagerFactory naprawdę jest w kontekście, można tak. I to też wygodniej postrzegać jako krótką diagnostykę, a nie nową obowiązkową klasę projektu.
import jakarta.persistence.EntityManagerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
public class JpaInfraCheck {
public static void check(ApplicationContext context) {
// Sprawdzamy obecność standardowego beana po nazwie (typowy default Spring Boot)
System.out.println(context.containsBean("entityManagerFactory")); // true
// I osobno sprawdzamy, że bean jest dostępny po typie
System.out.println(context.getBean(EntityManagerFactory.class).getClass().getName());
// org.hibernate.internal.SessionFactoryImpl (lub podobna klasa Hibernate)
}
}
Tu może dziwić linia z klasą Hibernate. I to normalne: JPA — to specyfikacja, a Hibernate — implementacja. Czyli EntityManagerFactory jako interfejs JPA masz, ale „pod spodem" będzie to obiekt Hibernate.
Jeszcze jedna ważna myśl: EntityManagerFactory nie „zastępuje DataSource". Jest zbudowany nad DataSource i używa go, żeby pobierać połączenia. Czyli DataSource — to transport, a EntityManagerFactory — już „stacja metra", która organizuje, jak po tym transporcie jeździć w kategoriach ORM.
Na razie wystarczy odnotować: po dodaniu startera aplikacja ma infrastrukturę JPA w postaci fabryki. W kolejnych dniach dodamy encje i zobaczymy, jak ta fabryka zaczyna pracować „w sprawie".
PlatformTransactionManager: menedżer transakcji
Trzeci kluczowy komponent — menedżer transakcji. Na poziomie SQL znamy już model: begin → kilka operacji → commit/rollback. Ale w Spring nikt nie pisze commit() ręcznie w każdym serwisie — inaczej byłby to kurs „jak cierpieć elegancko". Zamiast tego Spring daje komponent infrastrukturalny, który umie rozpoczynać i kończyć transakcje oraz przywiązywać je do wykonania twoich metod.
Właśnie dlatego menedżer transakcji — to część infrastruktury, a nie logiki biznesowej. Nie wie, co to „złożyć zamówienie" ani „zmniejszyć stany". Wie tylko granice transakcji, reguły i wynik wykonania. To jak skrupulatny księgowy: nieważne, co kupujesz, ważne, że operacja albo jest przeprowadzona, albo się cofnęła.
W Spring ogólny interfejs nazywa się PlatformTransactionManager. Dla stosu JPA typowa implementacja — JpaTransactionManager. I historia jest taka sama: typ beana można raz zobaczyć w kontekście i już nie wierzyć na słowo. Jak poprzednio, to nie powód, żeby zamieniać aplikację w wieczny rytuał println-ów.
Mini-weryfikacja (podobna do tego, co już robiliśmy):
import org.springframework.transaction.PlatformTransactionManager;
public class TxInfraCheck {
public static void check(PlatformTransactionManager txManager) {
// Sprawdzamy, że Spring naprawdę dał implementację JPA menedżera transakcji
System.out.println(txManager.getClass().getName());
// org.springframework.orm.jpa.JpaTransactionManager
}
}
Na razie nie używamy @Transactional w tym dniu — to będzie później, w module o transakcjach. Ale już teraz przydatne jest rozumienie: gdy tylko dodałeś starter JPA, Spring Boot przygotowuje podstawę pod przyszłe transakcje. I gdy potem postawisz @Transactional na metodzie serwisu, Spring będzie opierał się właśnie na tym menedżerze, żeby robić prawdziwe commit/rollback na poziomie bazy.
4. Łańcuch: kod → transakcje → JPA → JDBC → PostgreSQL
Teraz mamy trzy komponenty, i początkujący często widzi je jako trzy osobne „dziwne beany". Żeby złożyły się w zrozumiały obraz, zbierzmy je w jednym schemacie. Wyobraź sobie, że w przyszłości będziesz miał serwis, który wykonuje jakąś operację z danymi. Co w tym momencie dzieje się wewnątrz aplikacji?
Poniżej — uproszczony schemat. Nie pokazuje subtelności jak persistence context (to będzie dużo później), ale daje właściwy szkielet rozumienia.
flowchart TD
A["Twój kod service/repository"] --> B["PlatformTransactionManager JpaTransactionManager"]
B --> C["EntityManagerFactory"]
C --> D["EntityManager"]
D --> E["DataSource"]
E --> F["Połączenie JDBC"]
F --> G[(PostgreSQL)]
Sens łańcucha jest taki: menedżer transakcji ustawia ramę transakcyjną wokół operacji; infrastruktura JPA zapewnia działanie ORM wewnątrz tej ramy; DataSource wydaje prawdziwe połączenie; przez to połączenie SQL trafia do PostgreSQL. Tak, później zobaczymy niuanse jak cache pierwszego poziomu, dirty checking i flush. Ale jeśli teraz rozumiesz podstawową „rurę", pozbawisz się połowy mistycznych lęków wokół JPA.
Dla utrwalenia warto jeszcze raz krótko zestawić odpowiedzialności:
| Komponent | Główne pytanie, na które odpowiada |
|---|---|
| DataSource | „Skąd wziąć połączenie z bazą?" |
| EntityManagerFactory | „Jak stworzyć kontekst JPA do pracy z encjami?" |
| PlatformTransactionManager | „Jak poprawnie rozpocząć i zakończyć transakcję wokół operacji?" |
I zauważ: te komponenty nie „rywalizują" ze sobą i nie duplikują ról. Stoją jeden na drugim, jak warstwy ciasta. Nie tego ciasta, które „tylko kawałeczek", lecz tego, po którym mówisz: „no dobra, jeszcze jeden".
5. Typowe błędy przy uruchamianiu stosu JPA
Błąd nr 1: uważanie, że starter „podłącza bazę samodzielnie", nawet jeśli nie podałeś parametrów połączenia.
spring-boot-starter-data-jpa naprawdę podnosi infrastrukturę, ale i tak potrzebuje konkretnego URL JDBC, loginu i hasła. Jeśli nie podałeś ustawień, Boot nie zdoła stworzyć działającego DataSource. To nie „kaprys frameworka", to po prostu brak adresu, pod który jechać.
Błąd nr 2: mylenie sterownika JDBC i DataSource.
Sterownik — to biblioteka, która umie rozmawiać z PostgreSQL po protokole. DataSource — to obiekt w twojej aplikacji, który wydaje połączenia. Czasem studenci mówią: „mam DataSource, więc sterownik nie jest potrzebny". W praktyce bez sterownika DataSource albo się nie stworzy, albo nie zdoła stworzyć połączenia. Sterownik — to jak koła w samochodzie: możesz pięknie urządzić wnętrze, ale bez kół daleko nie zajedziesz.
Błąd nr 3: myślenie, że EntityManagerFactory — to „jeszcze jedna nazwa dla połączenia z bazą".
EntityManagerFactory dotyczy infrastruktury JPA, a nie „połączenia sieciowego". Żyje nad DataSource i pomaga pracować z encjami. Jeśli pomiesza się te poziomy w głowie, później zaczną się trudności: gdzie konfiguruje się połączenie, a gdzie żyje ORM.
Błąd nr 4: próba „sprawdzenia wszystkiego" przez kod biznesowy, zanim sprawdzisz infrastrukturę.
Bardzo typowa historia: student od razu pisze encję, repozytorium i serwis, a potem aplikacja się wysypuje, bo nie jest podłączona do bazy. W efekcie debugujesz naraz cztery problemy (kod + konfiguracja + baza + zależności) zamiast jednego. Dużo spokojniej jest najpierw zrobić smoke-check: czy jest DataSource, czy wydaje połączenie, czy podniósł się menedżer transakcji.
Błąd nr 5: zamienianie tymczasowej diagnostyki w stały styl.
Drukowanie beanów z main() — to dobre latarnia edukacyjna, ale zły styl dla stałego życia projektu. Używaj tego jako narzędzia rozumienia: sprawdziłeś, przekonałeś się, usunąłeś. Nie chcemy, żeby main() z czasem zamienił się w wysypisko eksperymentów, bo dalej będzie trudno utrzymać szkielet projektu w czystości.
GO TO FULL VERSION