1. Інфраструктура до JPA
Перш ніж зʼявляться сутності, у застосунку має бути інфраструктура, яка фізично вміє спілкуватися з базою даних. Якщо ви новачок, бажання «давайте вже писати @Entity і зберігати товари» абсолютно зрозуміле. Але без цієї інфраструктури ми будуємо будинок із красивими шпалерами й без фундаменту. Він тримається рівно до першого вітру, тобто до першої помилки підключення.
У цій лекції ми наводимо лад у голові: які саме компоненти піднімаються в застосунку на Spring Boot, коли ви додаєте JPA-стартер. Наша мета — прибрати відчуття «магії стартера» й замінити його на нормальну інженерну картину: хто за що відповідає, як компоненти повʼязані й як переконатися, що вони справді існують у контексті Spring.
Тут ми не розбираємо URL, логін і пароль. Зараз важливіше зрозуміти, що саме зʼявляється в застосунку і чому без цього JPA не запрацює.
2. Склад spring-boot-starter-data-jpa
Коли ви додаєте залежність spring-boot-starter-data-jpa, ви не просто «підключаєте бібліотеку для анотацій @Entity». Ви фактично кажете Spring Boot: «Шановний Boot, хочу JPA-стек для роботи з даними. Зберіть мені інфраструктуру, будь ласка». І Boot починає це робити, спираючись на дві речі: наявність потрібних класів у classpath і налаштування, які ми задамо в наступній лекції.
Корисно відразу тримати в голові дві ролі стартера. По-перше, він підтягує правильний комплект залежностей: JPA, ORM, Spring Data і транзакції. По-друге, запускає ланцюжок автонастроювання, який створює біни на кшталт DataSource, EntityManagerFactory і PlatformTransactionManager. Це і є та сама «магія», яка насправді доволі передбачувана, якщо знати, що саме має зʼявитися.
Мінімальний фрагмент Gradle-залежностей сьогодні має такий вигляд:
dependencies {
// JPA, Spring Data, транзакції та автонастроювання інфраструктури
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// Драйвер потрібен під час запуску й роботи застосунку, але зазвичай не потрібен під час компіляції коду
runtimeOnly("org.postgresql:postgresql")
}
Зверніть увагу на слово runtimeOnly біля драйвера PostgreSQL. Це не «каприз автора курсу» й не естетика. Це буквально те, без чого Spring Boot може підняти красиву JPA-інфраструктуру, а потім усе одно впасти, тому що спілкуватися з PostgreSQL йому просто нічим.
3. Компоненти JPA-стека
Нижче будуть короткі перевірочні фрагменти. Це не частина постійного каркаса проєкту, а одноразові перевірки: побачили реальний бін, зрозуміли його роль і прибрали код. Так простіше зняти відчуття магії й не перетворювати main() на склад println-діагностики.
JDBC-драйвер PostgreSQL
Про драйвер легко забути: в інтернет-прикладах він часто «десь там уже є», і все начебто працює само. Але якщо пояснювати чесно, драйвер — це перекладач між застосунком і базою даних. PostgreSQL не розуміє Java-обʼєкти, а Spring не вміє телепатично читати таблиці. Потрібен шар, який говорить із БД за її протоколом і реалізує стандартний JDBC API.
У вашому застосунку драйвер зазвичай не фігурує в коді напряму. Ви не створюєте new org.postgresql.Driver() (і слава богу). Але під час старту застосунку Spring Boot має змогти зрозуміти: «Так, база — PostgreSQL, отже драйвер такий-то». І далі вже через JDBC створюються реальні зʼєднання, через які надсилаються SQL-команди.
Корисна самоперевірка: якщо ви додали spring-boot-starter-data-jpa, але забули драйвер PostgreSQL, застосунок може пройти частину старту і потім впасти з помилкою на кшталт «no suitable driver». У цей момент корисно не проклинати ORM, а спокійно сказати собі: «Окей, JPA-інфраструктура у мене є тільки в теорії, а транспорту до бази немає».
Ланцюжок має скластися в голові так: PostgreSQL (сервер) ← JDBC driver ← DataSource ← JPA. Без середньої ланки все інше перетворюється на філософію.
DataSource: звідки беруться зʼєднання
DataSource — один із головних обʼєктів сьогоднішнього дня, тому що відповідає за просту й сувору річ: «дай мені зʼєднання з базою». У світі Java базова одиниця спілкування з БД — це Connection. Створювати його дорого, відкривати на кожен запит не можна, закривати потрібно акуратно. Тому в нормальному застосунку ви майже ніколи не створюєте зʼєднання вручну: ви отримуєте їх через DataSource, а далі інфраструктура керує їхнім життєвим циклом.
Корисна аналогія (із легкою самоіронією): база даних — це ресторан, Connection — це столик, а DataSource — це хостес на вході. Ви не повинні ломитися на кухню і шукати вільний столик самі. Ви кажете: «Мені б столик на двох» — і вас або садять, або ввічливо повідомляють: «Місць немає» (і це теж важлива інформація).
Важливо розрізняти кілька схожих слів, тому що на них новачки спотикаються:
| Термін | Що це | Приблизна «тривалість життя» |
|---|---|---|
| PostgreSQL | процес/сервер БД (у нашому випадку — контейнер) | поки працює docker compose up |
| JDBC-драйвер | бібліотека в застосунку | стільки ж, скільки живе застосунок |
| DataSource | фабрика або провайдер зʼєднань | зазвичай один бін на весь застосунок |
| Connection | конкретне зʼєднання | коротко: «на час роботи з БД» (часто — у межах транзакції) |
Найприємніше: Spring Boot після підключення JPA-стека і коректних налаштувань створює DataSource автоматично, і ви можете буквально побачити його в контексті. Нижче — одноразова перевірка, щоб зняти відчуття магії. Тримати такий код у main() як постійний каркас проєкту не потрібно.
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) {
// Одноразово запускаємо Spring-контекст, щоб подивитися,
// які інфраструктурні біни справді створилися
var context = SpringApplication.run(ShopDataJpaApplication.class, args);
// Дивимося, який DataSource піднявся (зазвичай це HikariCP)
System.out.println(context.getBean(DataSource.class).getClass().getName());
// com.zaxxer.hikari.HikariDataSource
// Дивимося, який менеджер транзакцій використовується для JPA
System.out.println(context.getBean(PlatformTransactionManager.class).getClass().getName());
// org.springframework.orm.jpa.JpaTransactionManager
}
}
Побачили реальні класи бінів — і цього вже достатньо. Сенс тут не в тому, щоб назавжди поселити System.out.println у точці входу, а в тому, щоб один раз побачити: інфраструктура справді піднялася.
Так, дорогою ми вже побачили Hikari і JpaTransactionManager. Але лякатися не потрібно: у цій лекції ми не «вивчаємо HikariCP», а просто фіксуємо факт — це реальні обʼєкти, а не казкові істоти з лісу автонастроювання.
Ще один маленький, але показовий фрагмент: якщо DataSource піднявся, ви можете отримати Connection і подивитися, до чого він реально підключився. Це така сама тимчасова нижньорівнева перевірка, а не постійний стиль точки входу.
import java.sql.Connection;
import javax.sql.DataSource;
public class DbSmoke {
public static void ping(DataSource dataSource) throws Exception {
// Важливо: зʼєднання обов'язково закриваємо.
// Для пулу це означає «повернути в пул», а не «знищити фізичне зʼєднання».
try (Connection c = dataSource.getConnection()) {
// Швидка перевірка: який URL реально використовує JDBC
System.out.println(c.getMetaData().getURL()); // jdbc:postgresql://localhost:5432/shop
// Ще одна проста smoke-перевірка: зʼєднання живе?
System.out.println(c.isValid(2)); // true
}
}
}
Тут важливо, що зʼєднання закривається через try-with-resources. Навіть якщо зʼєднання насправді надходять із пулу, закривати їх усе одно потрібно: для пулу це означає «повернути зʼєднання назад», а не «зруйнувати його фізично». Але це вже плавний місток до наступної лекції про пул.
EntityManagerFactory: звідки береться EntityManager
Після DataSource легко вирішити: «ну все, я підключився до бази». І це правда — але тільки на рівні JDBC. JPA — наступний поверх. У цьому світі основний робочий обʼєкт — EntityManager. Він дає змогу працювати із сутностями та запитами не як із «сирим SQL у рядках», а через ORM-модель.
Але EntityManager не створюється сам по собі. Його створює фабрика — EntityManagerFactory. І це важливо вкласти в голову заздалегідь, щоб потім не плутати рівні інфраструктури.
Грубо кажучи, без занурення у внутрішності: EntityManagerFactory — важкий обʼєкт, який створюється під час старту застосунку і живе довго. А EntityManager — більш «короткоживучий» обʼєкт, який використовується всередині операцій роботи з даними, часто в межах транзакції. Фабрика потрібна для того, щоб не збирати JPA-світ заново для кожного запиту.
Перевірити, що EntityManagerFactory справді є в контексті, можна так. І це теж зручніше сприймати як коротку діагностику, а не як новий обовʼязковий клас проєкту.
import jakarta.persistence.EntityManagerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
public class JpaInfraCheck {
public static void check(ApplicationContext context) {
// Перевіряємо наявність стандартного біну за імʼям (типова назва за замовчуванням у Spring Boot)
System.out.println(context.containsBean("entityManagerFactory")); // true
// І окремо перевіряємо, що бін доступний за типом
System.out.println(context.getBean(EntityManagerFactory.class).getClass().getName());
// org.hibernate.internal.SessionFactoryImpl (або схожий клас Hibernate)
}
}
Тут може збентежити рядок із класом Hibernate. І це нормально: JPA — специфікація, а Hibernate — реалізація. Тобто EntityManagerFactory як інтерфейс JPA у вас буде, але «під капотом» це виявиться обʼєкт Hibernate.
Ще одна важлива думка: EntityManagerFactory не «замінює DataSource». Він побудований поверх DataSource і використовує його, щоб отримувати зʼєднання. Тобто DataSource — це транспорт, а EntityManagerFactory — уже «станція метро», яка організує, як цим транспортом їздити в термінах ORM.
Поки нам достатньо зафіксувати: після підключення стартера в застосунку зʼявляється JPA-інфраструктура у вигляді фабрики. Далі ми додамо сутності й побачимо, як ця фабрика почне реально працювати в роботі.
PlatformTransactionManager: менеджер транзакцій
Третій ключовий компонент — менеджер транзакцій. На SQL-рівні ми вже знаємо модель: begin → кілька операцій → commit/rollback. Але в Spring ніхто не пише commit() руками в кожному сервісі — інакше це був би курс «як страждати красиво». Замість цього Spring дає інфраструктурний компонент, який уміє починати й завершувати транзакції та привʼязувати їх до виконання ваших методів.
Саме тому менеджер транзакцій — частина інфраструктури, а не бізнес-логіки. Він не знає, що таке «оформити замовлення» або «зменшити залишки». Він знає тільки межі транзакції, правила й результат виконання. Це як акуратний бухгалтер: йому не важливо, що ви купуєте, важливо, що операція або проведена, або відкотилася.
У Spring загальний інтерфейс називається PlatformTransactionManager. Для JPA-стека типовою реалізацією є JpaTransactionManager. І тут історія та сама: тип біну можна один раз побачити в контексті й більше не вірити на слово. Як і раніше, це не привід перетворювати застосунок на вічний println-ритуал.
Мініперевірка (схожа на ту, що ми вже робили):
import org.springframework.transaction.PlatformTransactionManager;
public class TxInfraCheck {
public static void check(PlatformTransactionManager txManager) {
// Перевіряємо, що Spring справді віддав JPA-реалізацію менеджера транзакцій
System.out.println(txManager.getClass().getName());
// org.springframework.orm.jpa.JpaTransactionManager
}
}
Поки що ми не використовуємо @Transactional — це буде пізніше, у модулі про транзакції. Але вже зараз корисно розуміти: щойно ви підключили JPA-стартер, Spring Boot готує основу під майбутні транзакції. І коли ви пізніше поставите @Transactional на метод сервісу, Spring спиратиметься саме на цей менеджер, щоб робити справжні commit/rollback на рівні БД.
4. Звʼязка: код → транзакції → JPA → JDBC → PostgreSQL
Зараз у нас три компоненти, і новачок часто бачить їх як три окремі «дивні біни». Щоб вони склалися в зрозумілу картинку, давайте зберемо їх в одну схему. Уявіть, що в майбутньому у вас буде сервіс, який виконує якусь операцію з даними. Що в цей момент відбувається всередині застосунку?
Нижче — спрощена схема. Вона не показує тонкощі на кшталт persistence context (це буде значно пізніше), але дає правильний каркас розуміння.
flowchart TD
A["Ваш код сервісу або репозиторію"] --> B["Менеджер транзакцій: PlatformTransactionManager / JpaTransactionManager"]
B --> C["EntityManagerFactory"]
C --> D["EntityManager"]
D --> E["DataSource"]
E --> F["JDBC Connection"]
F --> G[(PostgreSQL)]
Сенс ланцюжка такий: менеджер транзакцій задає транзакційні рамки навколо операції; JPA-інфраструктура забезпечує роботу ORM у межах цих рамок; DataSource видає реальне зʼєднання; через це зʼєднання SQL іде в PostgreSQL. Так, пізніше ми побачимо нюанси на кшталт кешу першого рівня, dirty checking і flush. Але якщо ви зараз зрозумієте базову «трубу», то позбавите себе половини містичних страхів навколо JPA.
Щоб закріпити, корисно ще раз коротко зіставити відповідальність:
| Компонент | Головне питання, на яке він відповідає |
|---|---|
| DataSource | «Звідки взяти зʼєднання з БД?» |
| EntityManagerFactory | «Як створити JPA-контекст для роботи із сутностями?» |
| PlatformTransactionManager | «Як правильно почати і завершити транзакцію навколо операції?» |
І зверніть увагу: ці компоненти не «змагаються» один з одним і не дублюють ролі. Вони стоять один на одному, як шари пирога. Не того пирога, який «я тільки шматочок», а того, після якого кажете: «ну гаразд, ще один».
5. Типові помилки під час запуску JPA-стека
Помилка № 1: вважати, що стартер «підключає базу сам», навіть якщо ви не вказали параметри підключення.
spring-boot-starter-data-jpa справді піднімає інфраструктуру, але йому все одно потрібні конкретні JDBC URL, логін і пароль. Якщо ви не вказали налаштування, Boot не зможе створити робочий DataSource. Це не «примха фреймворку», а просто відсутність адреси, куди їхати.
Помилка № 2: плутати JDBC-драйвер і DataSource.
Драйвер — це бібліотека, яка вміє говорити з PostgreSQL за протоколом. DataSource — обʼєкт у вашому застосунку, який видає зʼєднання. Іноді студенти кажуть: «у мене є DataSource, отже драйвер не потрібен». На практиці без драйвера DataSource або не створиться, або не зможе створити зʼєднання. Драйвер — це як колеса в автомобілі: ви можете красиво налаштувати салон, але без коліс далеко не поїдете.
Помилка № 3: думати, що EntityManagerFactory — це «ще одна назва для підключення до БД».
EntityManagerFactory належить до JPA-інфраструктури, а не до «мережевого зʼєднання». Він живе поверх DataSource і допомагає вам працювати із сутностями. Якщо змішати ці рівні в голові, пізніше почнеться плутанина: де налаштовується підключення, а де живе ORM.
Помилка № 4: намагатися «перевірити все» через бізнес-код до того, як ви перевірили інфраструктуру.
Дуже типова історія: студент одразу пише сутність, репозиторій і сервіс, а потім застосунок падає, тому що не підключений до бази. У результаті ви налагоджуєте одразу чотири проблеми (код + конфіг + база + залежності) замість однієї. Значно спокійніше спочатку зробити smoke-перевірку: чи є DataSource, чи видає він зʼєднання, чи піднявся менеджер транзакцій.
Помилка № 5: перетворювати тимчасову діагностику на постійний стиль.
Друк бінів із main() — хороший навчальний ліхтарик, але поганий стиль для постійного життя проєкту. Використовуйте це як інструмент розуміння: подивилися, переконалися, видалили. Ми не хочемо, щоб main() з часом перетворився на смітник експериментів, інакше далі буде складно тримати каркас проєкту чистим.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ