JavaRush /Курси /Spring Data JPA /JPA-стартер: DataSource

JPA-стартер: DataSource, EntityManagerFactory, Tx

Spring Data JPA
Рівень 4 , Лекція 1
Відкрита

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() з часом перетворився на смітник експериментів, інакше далі буде складно тримати каркас проєкту чистим.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ