1. Між сервісним методом і SQL — не порожній простір
Після першої лекції вже зрозуміло, куди дивитися: проблема не в тому, що Hibernate іноді поводиться «дивно», а в тому, що ми надто часто уявляємо його як тонку надбудову між репозиторієм і базою. На практиці там живе значно цікавіша система. Якщо цього не побачити, усі наступні теми курсу розсиплються на окремі ефекти.
Візьмімо ще раз дуже спокійний сервісний метод. Він майже навмисно виглядає безневинно:
@Service // Компонент Spring: клас буде створено як bean
public class CatalogService {
private final ProductRepository products; // Репозиторій Spring Data як точка входу в роботу з даними
public CatalogService(ProductRepository products) {
this.products = products; // Впровадження залежності (DI)
}
@Transactional(readOnly = true) // Транзакційна межа навіть для читання (це важливо для контексту)
public Product load(long id) {
// findById може повернути Optional.empty(), тому orElseThrow
return products.findById(id).orElseThrow();
}
}
Зазвичай такий код читають так: «репозиторій сходив у базу і приніс Product». Для звичайного CRUD-мислення цього достатньо. Але для глибокого розбору — замало. Тут важливіше інше запитання: що саме відбувається між викликом load() і SQL-запитом у PostgreSQL?
Зручно тримати в голові таку спрощену схему:
flowchart TD
%% Важливо: це не «магія», а послідовність рівнів і компонентів
A[Сервісний метод] --> B[Репозиторій Spring Data]
B --> C[EntityManager]
C --> D[Hibernate Session]
D --> E[Persistence Context]
E --> F[Flush / генерація SQL]
F --> G[(PostgreSQL)]
Ця схема потрібна не лише для наочності. Вона перетворює ORM із «чогось під капотом» на набір цілком спостережуваних рівнів. Репозиторій — зручний вхід. EntityManager — JPA-рівень керування сутностями та запитами. Session — нативний Hibernate-рівень. Persistence Context — робоча памʼять поточної операції. Flush — фаза синхронізації. А generated SQL — той самий слід у логах, який можна перевірити власними очима.
І ось тут зʼявляється перший момент прозріння: більшість ORM-симптомів — це не набір окремих багів, а прояви одного й того самого механізму роботи. Поки ви не бачите цю ланку, Hibernate здається магією. Щойно бачите — магія помітно здувається, і залишається інженерна система зі своєю логікою.
2. EntityManager і Session: два рівні одного стеку
Розробники часто чули слова EntityManager і Session, але не завжди чітко розуміють, як вони співвідносяться. Через це виникає хибне відчуття, ніби це дві конкуруючі моделі роботи з ORM. Насправді все простіше: EntityManager — це стандартний JPA-інтерфейс, а Session — нативний Hibernate-інтерфейс, який стоїть під ним. Вони не конкурують між собою. Вони описують один і той самий runtime на двох рівнях абстракції.
Якщо прибрати Spring Data і переписати читання ближче до JPA, вийде так:
@Service // Звичайний сервіс Spring
public class ProductFinder {
private final EntityManager entityManager; // JPA-вхід: керує сутностями та запитами в межах контексту
public ProductFinder(EntityManager entityManager) {
this.entityManager = entityManager; // DI: сюди Spring підставить проксі або реальний EntityManager
}
@Transactional(readOnly = true) // Транзакція потрібна, щоб контекст був «живим» упродовж методу
public Product load(long id) {
// Прямий JPA-виклик: пошук за первинним ключем
return entityManager.find(Product.class, id);
}
}
Це вже допомагає побачити важливу річ: репозиторій Spring Data не замінює JPA — він працює через неї. А ключовий JPA-вхід в операції із сутностями та запитами — саме EntityManager. 📎
Якщо ж вам потрібно зазирнути на рівень Hibernate безпосередньо, ви не потрапляєте в інший всесвіт. Ви просто розкриваєте поточну сесію:
@Service // Сервіс-адаптер між JPA та Hibernate API
public class HibernateBridge {
private final EntityManager entityManager; // Усе починається з EntityManager
public HibernateBridge(EntityManager entityManager) {
this.entityManager = entityManager;
}
public Session currentSession() {
// unwrap: отримуємо нативний Hibernate Session із JPA EntityManager
return entityManager.unwrap(Session.class);
}
}
Цей рядок добре знімає зайву містичність зі стеку. Session — не «ще одна ORM поряд». Це той самий робочий контекст Hibernate, через який JPA в нашому застосунку і виконується. Для всього курсу цього розуміння поки що достатньо: працюючи через Spring Data, ви не обходите Hibernate — ви просто дивитеся на нього через вищий шар.
3. Persistence Context: робоча памʼять ORM 🧠
Тепер найважливіше слово цієї лекції — persistence context. Звучить трохи академічно, але за змістом це дуже практична річ. Hibernate не працює як примітивний проксі «викликав метод — одразу SQL — усе забули». Він тримає робочий стан усередині поточної операції. Цей стан і є persistence context.
У Spring є свій ApplicationContext, де він зберігає beans. А у Hibernate — свій робочий контекст, той самий persistence context, де живуть entities. Усе в цьому світі циклічне.
Якщо хочете зовсім простий образ, думайте про persistence context як про робочу памʼять ORM. У межах однієї операції Hibernate памʼятає, які сутності вже були завантажені, які з них зараз managed, які зміни відбулися, які звʼязки вже ініціалізовані, а які ще ліниві, і що взагалі потрібно буде синхронізувати з базою. Це не теоретична конструкція. Це реальна частина runtime-поведінки.
Звідси одразу випливають кілька дуже практичних наслідків. По-перше, усередині одного контексту Hibernate намагається тримати identity сутності стабільною: один і той самий рядок має відповідати одному й тому самому managed-обʼєкту. По-друге, повторні звернення до вже завантаженої сутності обробляються інакше, ніж перше читання. По-третє, зміна обʼєкта в памʼяті — це вже подія для ORM, навіть якщо ви поки не відправили жодного SQL.
Нам сьогодні не потрібно повністю розпаковувати стани transient, managed, detached і removed. Це окрема серйозна розмова, і вона незабаром стане центральною. Поки достатньо побачити сам принцип: між вашим кодом і БД існує живий контекст, а не порожній канал. Якщо ви не тримаєте це в голові, поведінка Hibernate справді здається спонтанною. Якщо тримаєте — багато речей раптово стають пояснюваними.
4. Transaction boundary: де живе unit of work
persistence context майже завжди варто бачити разом із ще одним поняттям — transaction boundary. У типовому Spring-застосунку цю межу задає @Transactional на сервісному методі. І це не просто «анотація для роботи з базою». За змістом це рамка однієї цілісної операції з даними. Саме всередині цієї рамки Hibernate і отримує шанс тримати свій робочий контекст і поводитися як ORM, а не як набір випадкових викликів SQL.
Дуже важливо не зводити транзакцію до формули «один SQL-запит». Типовий сценарій цілком може включати читання кількох сутностей, зміну частини графа, виконання додаткових перевірок і лише потім синхронізацію результату з базою. У цьому й полягає ідея unit of work: ви мислите не окремими командами JDBC, а однією узгодженою операцією застосунку.
Подивіться на простий приклад:
@Service // Сервісний рівень
public class OrderPlacementService {
private final ProductRepository products; // Пошук товарів
private final PurchaseOrderRepository orders; // Збереження замовлення
public OrderPlacementService(ProductRepository products, PurchaseOrderRepository orders) {
this.products = products;
this.orders = orders;
}
@Transactional // Уся операція виконується в одній транзакції та в одному persistence context
public void placeOrder(long productId) {
// 1) Завантажуємо сутність: вона стає managed у поточному контексті
Product product = products.findById(productId).orElseThrow();
// 2) Створюємо новий обʼєкт (ще transient)
PurchaseOrder order = new PurchaseOrder();
order.setOrderNumber("ORD-1001"); // Заповнюємо поля доменної моделі
// 3) Зберігаємо: обʼєкт стає persistent/managed (у термінах контексту)
orders.save(order);
// Тут важливий не синтаксис, а сама ідея:
// одна бізнес-операція живе в одній транзакційній межі.
}
}
Межа транзакції тут важливіша за конкретні рядки коду. Саме вона відповідає на запитання, де починається і закінчується поточна операція, де взагалі існує поточний persistence context і в який момент Hibernate зобовʼязаний привести його у відповідність із базою. Коли потім ви будете діагностувати lazy loading, merge, bulk side effects або locking, це запитання стане базовим. Без нього далі не зрушити.
5. Flush і generated SQL: момент істини
І ще одна ключова річ: Hibernate не зобовʼязаний надсилати SQL рівно в тому рядку Java, де ви подумки «здійснили дію». Це незвично, якщо ви довго жили в моделі «викликав update() — SQL пішов». В ORM між кодом і БД спочатку накопичується стан, а потім настає окрема фаза синхронізації. Ця фаза і називається flush.
Наразі нам не потрібен повний перелік flush-режимів. Важливо побачити принцип: flush — це не побутове «майже commit», а окремий момент, коли Hibernate переводить поточний стан контексту в реальні SQL-команди. Наприклад:
@Service // Сервісний рівень
public class ProductRenamer {
private final EntityManager entityManager; // Працюємо безпосередньо через JPA
public ProductRenamer(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional // Потрібна транзакція, щоб зміни відстежувалися й могли бути синхронізовані
public void rename(long id, String newName) {
// Завантажуємо сутність: далі Hibernate відстежуватиме зміни (dirty checking)
Product product = entityManager.find(Product.class, id);
product.setName(newName); // Змінюємо поле в managed-обʼєкта (SQL ще може не піти)
// Ми змінили managed-обʼєкт у памʼяті.
// SQL UPDATE зʼявиться в момент синхронізації, а не тут як окремий ручний виклик.
}
}
У логах це потім може проявитися так:
-- 1) Спершу читання: Hibernate завантажує рядок у persistence context
select p1_0.id,p1_0.name,p1_0.sku from product p1_0 where p1_0.id=?
-- 2) Потім UPDATE: результат flush/dirty checking (момент може бути не в рядку setName)
update product set name=? where id=?
І тут варто знову повернутися до фрази з минулої лекції: SQL — детектор брехні для ORM. Ви можете думати, що «нічого особливого не сталося». Але якщо в логах пішов UPDATE, значить для Hibernate це вже була цілком реальна операція синхронізації. Ви можете думати, що читання було «одним викликом репозиторію». Але якщо в логах видно десять запитів, значить use case для бази даних виглядав зовсім інакше. 🔬
Тому generated SQL у цьому курсі — не декоративний супровід. Це джерело правди. Ми не будемо вірити Java-коду на слово там, де можна власними очима перевірити, що застосунок насправді відправив у PostgreSQL. Це одна з найкорисніших звичок, яку ви заберете вже з першого дня.
6. Діагностична лінза першого дня
Якщо після цієї лекції у вас і має залишитися один переносний артефакт, нехай це буде не список термінів, а коротка діагностична лінза. Її можна тримати поруч із будь-яким дивним ORM-кейсом — від LazyInitializationException до зайвого SELECT. Вона не замінює глибокого розуміння, але чудово спрямовує погляд.
| Питання | Що це допомагає зʼясувати |
|---|---|
| Де проходить transaction boundary? | Без цього не можна зрозуміти, чи ще живий поточний контекст і чому lazy loading взагалі можливий або вже неможливий |
| Через який рівень іде робота — репозиторій, EntityManager, Session? | Допомагає побачити, на якому шарі ви зараз працюєте і де шукати фактичну поведінку |
| Що вже знаходиться в persistence context? | Пояснює повторні читання, identity map і реакцію Hibernate на зміни |
| Чи була зміна managed-обʼєкта, чи лише читання? | Саме тут починається розмова про dirty checking і можливий UPDATE |
| Чи міг тут відбутися flush? | Допомагає перестати чекати SQL «саме в цьому рядку коду» |
| Який generated SQL насправді пішов? | Зіставляє вашу інтуїцію з реальністю |
| Чи збігається SQL-профіль із use case? | Підказує, проблема у fetching, mapping, проєктуванні транзакцій чи в самому способі читання |
Ця таблиця хороша тим, що одразу робить курс практичним. Ви не просто почули терміни EntityManager, Session, flush і persistence context. У вас зʼявився робочий спосіб ставити запитання до поведінки Hibernate. І це вже не мотиваційна балаканина, а реальна практична користь.
7. Типові помилки розуміння runtime-моделі
Помилка № 1: думати, що репозиторій і є вся робота з БД.
Репозиторій — зручний фасад. Але під ним працює JPA-рівень, а під JPA — конкретний runtime Hibernate. Якщо цього не бачити, майже будь-яка нетривіальна проблема здаватиметься «аномалією».
Помилка № 2: сприймати EntityManager і Session як різні світи.
Насправді це різні рівні одного стеку. Плутанина тут зазвичай заважає не коду, а мисленню розробника: він просто не розуміє, де шукати фактичне джерело поведінки.
Помилка № 3: вважати persistence context абстракцією «для теорії».
Щойно ви бачите неочікуваний UPDATE, повторне завантаження, lazy proxy або identity-поведінку, ви вже маєте справу з контекстом. Він реальний, навіть якщо напряму не видно.
Помилка № 4: зводити flush до слова «commit».
Commit і flush повʼязані, але це не одне й те саме. На рівні першої робочої моделі достатньо запамʼятати: flush — окрема фаза синхронізації, і саме вона часто пояснює, «чому SQL пішов зараз».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ