1. Вступ
Коли ви вперше чуєте про StatelessSession, мозок зазвичай пропонує дві крайнощі: або «це якась стара дивна штука», або «о, це Hibernate, але швидше, терміново переписати все». Істина, як завжди, лежить десь посередині, і щоб її вловити, корисно чесно сформулювати проблему.
Масові update/delete чудові, коли бізнес-правило вкладається в одну SQL-команду: «усім активним товарам поставити статус HIDDEN» або «видалити всі snapshot, старші за 90 днів». Але бувають масові сценарії, де зміну не можна виразити однією спільною формулою, тому що рішення залежить від даних кожного рядка. Наприклад, «якщо SKU починається з LEGACY-, приховати товар», «якщо warrantyMonths = 0 — позначити як “без гарантії”», «для кожного запису snapshot перерахувати значення за новою формулою». Тоді bulk уже не допомагає, а entity-loop через звичайний EntityManager починає платити за можливості, які в цій задачі майже не потрібні.
І ось тут зʼявляється StatelessSession: це режим «я хочу працювати построково, але не хочу тягнути за собою persistence context, first-level cache та automatic dirty checking». Уявіть, що звичайний Hibernate — це машина з автопілотом, який сам стежить, куди ви повернули кермо (dirty checking), і сам вирішує, коли натиснути газ (flush). StatelessSession — це та сама машина, але ви зняли автопілот і сіли за механіку: усе чесно, усе вручну, зате без зайвих накладних витрат.
2. Що таке StatelessSession
Спочатку розберімося з терміном. StatelessSession — це Hibernate-specific API (не JPA), який надає прямий спосіб виконувати операції над сутностями, але без повноцінної managed-моделі. Це не означає, що сутності «без стану» (поля ж у них нікуди не зникають, не хвилюйтеся). Це означає, що сесія не поводиться як контейнер, який керує життєвим циклом обʼєктів.
Якщо звичайний Session/EntityManager живе в парадигмі unit of work (завантажили → змінили → Hibernate сам відстежив → на flush/commit синхронізував), то StatelessSession живе в парадигмі «виконав операцію — отримав ефект». Тут немає кешу першого рівня як identity map, немає snapshots для dirty checking, і тому просте змінення поля в обʼєкті не перетворюється саме по собі на SQL.
Щоб швидше зафіксувати картину, корисно поглянути на порівняння у вигляді таблиці. Ми не перетворюємо лекцію на довідник, просто зведемо головні відмінності в одну таблицю.
| Можливість / властивість | Звичайний Session / EntityManager | StatelessSession |
|---|---|---|
| First-level cache (identity map) | Є: один рядок → один managed-обʼєкт у межах контексту | Немає: повторні читання не «склеюються» в один обʼєкт |
| Automatic dirty checking | Є: змінили поле → Hibernate сам знайде зміни на flush | Немає: змінили поле → нічого не станеться, доки ви явно не скажете |
| Flush cycle | Є: зміни накопичуються й відправляються під час flush | Зазвичай операції виконуються явно і прямолінійно |
| Cascades / orphanRemoval як «автоматика графа» | Так (у межах коректного мапінгу) | Ні (тобто не розраховуйте на це) |
| Типова роль | Бізнес-операції, агрегати, звичайний service-layer | Технічні масові операції «построково й дешево» |
Головна думка тут дуже проста: StatelessSession — це вузькоспеціалізований інструмент. Він не замінює EntityManager. Він допомагає там, де звичайний EntityManager занадто «розумний» і дорогий для конкретної масової технічної задачі.
3. Модель роботи: «Hibernate сам» → «я сам»
У звичайному Hibernate ви звикли, що достатньо отримати managed-сутність, змінити поле, і далі ORM «сам розбереться». Для новачків це взагалі одна з найприємніших частин ORM: здається, що БД десь далеко, а ви просто працюєте з обʼєктами. Але це задоволення оплачується механікою persistence context.
У StatelessSession філософія інша: Hibernate перестає бути «спостерігачем» ваших обʼєктів. Він перетворюється на «виконавця команд». Ви явно кажете: прочитай (get), встав (insert), онови (update), видали (delete). І так, спочатку це відчувається як «що за камʼяний вік, чому не можна просто викликати сетер». Але саме ця «камʼяність» і є причиною, чому StatelessSession іноді виграє в масових сценаріях.
Щоб це не було абстракцією, подивімося на контраст. У звичайному managed-flow змінення виглядає так:
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductStatusService {
private final EntityManager entityManager;
public ProductStatusService(EntityManager entityManager) {
// Інжектуємо JPA EntityManager: це managed-світ з усіма «автоматами»
this.entityManager = entityManager;
}
@Transactional
public void hideProduct(long productId) {
// 1) Завантажуємо сутність: вона стає managed у persistence context
Product p = entityManager.find(Product.class, productId);
// 2) Змінюємо поле: цього достатньо, далі вмикається dirty checking
p.setStatus(ProductStatus.HIDDEN); // UPDATE піде сам на flush/commit
}
}
А в StatelessSession той самий принцип виглядає так, якщо вважати, що сесія вже відкрита всередині транзакції. Тут нам потрібен лише один факт: сетер сам по собі SQL не створює.
import org.hibernate.StatelessSession;
public void hideProduct(StatelessSession ss, long productId) {
// Припускаємо, що ss уже відкрито всередині транзакції: зараз важлива сама механіка update()
Product p = ss.get(Product.class, productId);
// Сетер змінює поле лише в памʼяті
p.setStatus(ProductStatus.HIDDEN);
// Явна команда: без неї UPDATE не відбудеться
ss.update(p); // без цього UPDATE не буде
}
Якщо ви любите короткі формули, то ось вона: у StatelessSession дія над обʼєктом ≠ дія в БД. Дія в БД відбувається лише тоді, коли ви явно викликали метод, який цю дію описує.
4. Відкриття StatelessSession у Spring Boot
У Spring Boot + Spring Data JPA ми зазвичай живемо у світі EntityManager. І це нормально: JPA — наша спільна мова для persistence. Але StatelessSession — це Hibernate API, отже нам потрібен SessionFactory.
Найспокійніший шлях — отримати SessionFactory через EntityManagerFactory.unwrap(...). Це виглядає трохи інженерно, зате прозоро: ми явно визнаємо, що зараз використовуємо Hibernate-specific функціональність.
Невеликий провайдер (зручно покласти в com.example.commerce.labsupport, бо це інструмент для лабораторій і технічних сценаріїв):
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Component;
@Component
public class HibernateSessionFactories {
private final SessionFactory sessionFactory;
public HibernateSessionFactories(EntityManagerFactory emf) {
// Дістаємо нативний Hibernate SessionFactory з JPA-фабрики
this.sessionFactory = emf.unwrap(SessionFactory.class);
}
public SessionFactory sessionFactory() {
// Віддаємо SessionFactory тим компонентам, де потрібен Hibernate-specific API
return sessionFactory;
}
}
Тепер ми можемо відкрити StatelessSession там, де це справді потрібно. І важлива звичка: StatelessSession потрібно закривати. Тому використовуємо try-with-resources — нехай Java сама простежить за прибиранням після вечірки.
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
public void doWork(SessionFactory sessionFactory) {
// StatelessSession потрібно закривати вручну — тому try-with-resources
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
// Тут можна виконувати stateless-операції; для запису всередині цієї сесії все одно потрібна транзакція
}
}
Так, це виглядає трохи більш вручну, ніж @Transactional + EntityManager, але в цьому й сенс: це окремий режим роботи.
5. Міні-API StatelessSession
Нижче ми пройдемося по базових операціях StatelessSession на прикладі наших сутностей із Commerce Persistence Lab. Я спеціально триматиму приклади короткими, щоб ви могли читати їх як мініскрипти і не потонути в інфраструктурі.
get(): читання без identity map
У звичайному persistence context кеш першого рівня гарантує: «один рядок → один managed-обʼєкт». Це корисно і для коректності, і для економії запитів. У StatelessSession цього немає, тому повторне читання за одним і тим самим id може дати два різні обʼєкти у памʼяті. З погляду БД вони описують один рядок, але з погляду Java це різні екземпляри.
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
public void demonstrateIdentity(SessionFactory sessionFactory) {
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
// Два читання одного й того самого рядка
Product p1 = ss.get(Product.class, 1L);
Product p2 = ss.get(Product.class, 1L);
// Важливо: identity map немає, тому це різні екземпляри
System.out.println(p1 == p2); // false
}
}
Це не «баг Hibernate», а логічний наслідок: немає persistence context — немає й identity map. Тому в stateless-режимі не можна покладатися на обʼєктну ідентичність так, як ви звикли у managed-світі.
update(): зміни не записуються самі
Найчастіша «психологічна пастка» StatelessSession виглядає так: ви змінюєте поле в обʼєкті і чекаєте, що буде UPDATE. Але dirty checking відсутній, тому Hibernate не «помітить» змін автоматично. Вам потрібно явно викликати update().
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
public void hideProduct(SessionFactory sessionFactory, long productId) {
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
Transaction tx = ss.beginTransaction();
// 1) Завантажили сутність
Product p = ss.get(Product.class, productId);
// 2) Змінили поле в памʼяті
p.setStatus(ProductStatus.HIDDEN);
// 3) Явно попросили Hibernate зробити UPDATE в БД
ss.update(p); // без цього рядок у БД не зміниться
// 4) Зафіксували транзакцію
tx.commit();
}
}
Якщо ви хочете переконатися в поведінці, можна зробити контрольне читання в іншій сесії. Зверніть увагу: я спеціально ділю приклад на дві try-секції, щоб не змішувати читання і запис у межах одного блоку за звичкою.
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
public void checkStatus(SessionFactory sessionFactory, long productId) {
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
// Контрольне читання: це інша StatelessSession, без спільного контексту
Product p = ss.get(Product.class, productId);
// Дивимося, що реально лежить у базі
System.out.println(p.getStatus()); // наприклад: HIDDEN
}
}
Ця явність здається нудною рівно до того моменту, поки ви не починаєте масову обробку й не розумієте, що саме така нудна передбачуваність вам і потрібна.
insert(): явне вставлення
У звичайному EntityManager.persist() сутність стає managed, INSERT часто відкладається до flush, і навколо цього будується велика частина поведінки ORM. У StatelessSession ви дієте прямолінійно: створили обʼєкт, викликали insert(). Це добре підходить для технічних сценаріїв, коли ви не хочете накопичувати managed-контекст.
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
public void insertProduct(SessionFactory sessionFactory) {
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
Transaction tx = ss.beginTransaction();
// Збираємо нову сутність як звичайний POJO
Product p = new Product();
p.setSku("SKU-SS-001");
p.setName("Stateless USB Cable");
p.setStatus(ProductStatus.ACTIVE);
// Явна команда вставлення
Object id = ss.insert(p);
// Фіксуємо вставку в БД
tx.commit();
// Можна одразу використовувати повернений ідентифікатор
System.out.println("Вставлений id = " + id); // Наприклад: 123
}
}
Зверніть увагу, як змінюється відчуття часу: у managed-світі ви часто думаєте «INSERT буде потім, на flush». У stateless-світі ви думаєте «я зараз вставляю». Це психологічно корисно, коли ви пишете супровідний код і хочете менше неявності.
delete(): видалення як команда
З видаленням схожа історія: у managed-світі ви робите remove(), і реальний DELETE може піти на flush. У StatelessSession ви зазвичай спочатку читаєте обʼєкт (або отримуєте його іншим способом) і явно викликаєте delete().
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
public void deleteProduct(SessionFactory sessionFactory, long productId) {
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
Transaction tx = ss.beginTransaction();
// Спочатку отримуємо обʼєкт (хоча в деяких сценаріях можна обійтися без цього)
Product p = ss.get(Product.class, productId);
// Явна команда видалення
ss.delete(p);
// Фіксуємо транзакцію
tx.commit();
}
}
Тут важлива практична примітка: якщо ваша задача звучить як «видалити багато рядків за умовою», частіше чесніше використовувати bulk delete однією командою. StatelessSession.delete() — це все-таки построковий стиль, просто без persistence context. Ми ще порівняємо інструменти в наступній лекції, а тут запамʼятаємо роль: StatelessSession — не конкурент bulk, а інструмент для сценаріїв «построково, але без managed-навантаження».
6. Графи та каскади
Майже завжди, коли розробник намагається використовувати StatelessSession як «звичайний Hibernate, тільки швидше», він доволі швидко впирається в граф сутностей. У нашому проєкті це особливо помітно на PurchaseOrder і OrderItem: замовлення — це агрегат, у нього є колекція позицій, є посилання на Customer і Product, а в managed-світі ми акуратно живемо з каскадами, orphan removal та helper-методами.
StatelessSession влаштований інакше: він не про керування графом, а про виконання операцій. Тому розраховувати, що insert(order) автоматично правильно вставить усі OrderItem, — погана ідея. Ви або будете явно вставляти дочірні обʼєкти, або взагалі зрозумієте, що вам потрібен managed-підхід (і це нормально). На практиці це означає просте правило: StatelessSession частіше підходить для сценаріїв, де ви працюєте з відносно «плоскими» структурами або готові вручну контролювати порядок операцій.
Щоб відчути різницю, уявіть два стилі мислення.
У managed-стилі ви думаєте: «я зібрав граф замовлення в памʼяті, Hibernate сам синхронізує його коректно на flush». У stateless-стилі ви думаєте: «я зараз вставлю order, потім item1, потім item2…». Це більше схоже на JDBC, але з бонусом: мапінг сутностей усе ще використовується, вам не потрібно руками писати SQL для кожного стовпця.
І це не недолік, а огорожа від магії. Щойно ви бачите, що задача вимагає каскадів і складного графа, StatelessSession перестає бути підходящим інструментом. Він як маленький швейцарський ніж: ним зручно підрізати нитку, але будувати дім краще все-таки молотком і дрилем (так, аналогія трохи побутова, зате життєва).
7. Транзакції в StatelessSession
Іноді слово stateless провокує думку: «а, отже, можна без транзакцій». На жаль (або на щастя), PostgreSQL не вражений назвою інтерфейсу. Якщо ви змінюєте дані, транзакційна межа потрібна так само, як і у звичайному Hibernate-коді: щоб забезпечити атомарність, коректність і передбачувану поведінку під час помилок.
Найпряміший спосіб — керувати транзакцією прямо на StatelessSession. Так, це трохи нижчий рівень, але зате прозоро і добре підходить для технічних завдань.
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
public void statelessTx(SessionFactory sessionFactory, long productId) {
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
// Транзакції все одно потрібні: це рівень БД/JDBC, а не «фіча persistence context»
Transaction tx = ss.beginTransaction();
// Робимо набір команд у межах однієї транзакції
Product p = ss.get(Product.class, productId);
p.setStatus(ProductStatus.HIDDEN);
ss.update(p);
// Фіксуємо зміни
tx.commit();
// У реальному коді: додайте rollback у catch-блоці (тут залишаємо приклад коротким)
}
}
У реальному коді ви ще додасте обробку винятків і rollback(), але сам принцип важливіший за деталі: StatelessSession не скасовує необхідність думати транзакціями. Він просто прибирає persistence context і automatic dirty checking. А транзакція — це інший шар реальності: рівень JDBC і бази даних.
8. Де доречний StatelessSession у проєкті
Зараз найважливіше — не закохатися в інструмент надто рано. StatelessSession особливо добре підходить до задач із серії «технічне обслуговування даних» або «масова обробка, але з індивідуальним рішенням для кожного рядка». У Commerce Persistence Lab це найчастіше буде щось навколо каталогу та історичних даних.
Наприклад, ви хочете пройтися по списку id товарів (скажімо, ви отримали його зі звіту або окремого запиту) і для кожного товару виконати невелику правку: приховати, виправити службовий прапорець, оновити статус. Це вже не bulk update (бо список і правило можуть бути складними), але й звичайний EntityManager може бути важким, якщо список великий.
І так, stateless тут не скасовує транзакцію: ми все одно хочемо зафіксувати цей набір команд як одну операцію на рівні БД.
Мініескіз такого сценарію:
import java.util.List;
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
public void hideMany(SessionFactory sessionFactory, List<Long> ids) {
try (StatelessSession ss = sessionFactory.openStatelessSession()) {
Transaction tx = ss.beginTransaction();
// Stateless-режим не накопичує managed-обʼєкти і не робить dirty checking
for (Long id : ids) {
// Для кожного рядка: прочитали...
Product p = ss.get(Product.class, id);
// ...змінили в памʼяті...
p.setStatus(ProductStatus.HIDDEN);
// ...і явно записали в базу
ss.update(p);
}
tx.commit();
}
}
Зверніть увагу на ціну цього коду. Він не тягне persistence context, не накопичує тисячі managed-обʼєктів, не робить dirty checking по всьому графу. Але він і не намагається «бути розумнішим» за вашу задачу: ви явно сказали, що хочете зробити з кожним рядком.
І ось тут зʼявляється головний методичний висновок лекції: StatelessSession — це не нова філософія всього застосунку, а точковий режим, коли ви хочете «Hibernate-мапінг, але без managed-механіки».
9. Типові помилки під час роботи зі StatelessSession
Помилки навколо StatelessSession майже завжди не синтаксичні, а ментальні: розробник продовжує думати як у managed-світі, але код уже живе в stateless-світі. Тому нижче будуть не стільки «пастки API», скільки типові неправильні очікування — і як їх вчасно розпізнати за симптомами.
Помилка №1: використовувати StatelessSession як заміну EntityManager лише тому, що швидше.
На практиці це призводить до того, що ви втрачаєте зручні й важливі властивості managed-моделі (dirty checking, каскади, роботу з графом), а натомість отримуєте купу ручної роботи й ризик зробити неконсистентні зміни. StatelessSession добрий як інструмент для вузьких технічних сценаріїв, але як основний стиль бізнес-коду він зазвичай погіршує читабельність і надійність.
Помилка №2: змінювати поля в сутності й забувати викликати update().
Це класика. Код виглядає «майже як зазвичай», але в базі нічого не змінюється. У managed-світі сетер запускає ланцюжок dirty checking → flush. У stateless-світі сетер — це просто сетер, і на цьому все. Гарна звичка — мислити дієсловами: прочитали (get) → змінили → сказали Hibernate, що робити (update).
Помилка №3: очікувати, що get() двічі поверне один і той самий обʼєкт.
Якщо ви в алгоритмі використовуєте порівняння за посиланням (==) або зберігаєте обʼєкти в структурах, де ви розраховуєте на identity map, ви отримаєте «дивності». У StatelessSession немає first-level cache, тому повторні читання не зобовʼязані повертати той самий екземпляр обʼєкта.
Помилка №4: розраховувати на каскади та «магічне збереження графа».
StatelessSession не призначений для того, щоб ви зібрали складний граф замовлення з позиціями і сказали «ну, встав усе». Цей режим про явні операції, а не про lifecycle агрегатів. Якщо задача справді про агрегат і граф, часто чесніше повернутися до звичайного @Transactional managed-flow.
Помилка №5: змішувати EntityManager і StatelessSession в одному сценарії без розуміння наслідків.
Якщо ви вже завантажили Product через EntityManager (він managed і живе в persistence context), а потім оновили той самий товар через StatelessSession, то ваш managed-обʼєкт усе ще може тримати старі значення. За відчуттями це буде схоже на stale persistence context після bulk: база вже змінилася, а памʼять «не в курсі». Тому такі змішані сценарії потребують дисципліни: або розділяти фази (clear() / повторне читання), або не змішувати підходи без потреби.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ