1. Навіщо взагалі пригадувати стани entity
Після розмови про persistence context наступне питання виникає саме собою: чому один і той самий Product Hibernate відстежує, а інший — уже ні? Якщо цього не зрозуміти зараз, далі все виглядатиме як лотерея: десь зміна раптово «вилетіла» в БД, а десь ви змінюєте поля — і нічого не відбувається.
Якщо ви тільки починаєте, легко мислити так: «У мене є об’єкт Product — отже, це entity, і вона “в базі”». Але ORM влаштований хитріше: один і той самий Java-об’єкт може бути то під контролем Hibernate, то взагалі поза грою. Щоб не гадати, «чому зміни не збереглися» або «чому воно раптом видалилося», нам потрібен короткий робочий словник із чотирьох станів.
Ключова думка проста і певною мірою навіть образлива: стан entity — це не про те, чи є рядок у БД, і навіть не про те, заповнене поле id чи ні. Стан — це про те, чи знає поточний persistence context про цей об’єкт і чи відстежує його. Тобто це відношення об’єкта до контексту, а не його «велич» як таку.
В об’єкта немає таємного прапорця, який живе з ним вічно. Один і той самий Product може бути managed усередині сервісної транзакції й detached одразу після її завершення — просто тому, що контекст змінився або завершився. Якщо хочеться перевірити це не інтуїцією, а руками, у JPA є дуже корисний метод: entityManager.contains(entity). Він не розв’язує всі проблеми за вас, але швидко показує, чи відстежує ORM цей екземпляр саме зараз.
2. Чотири стани: таблиця
У цій таблиці зібрано стани так, щоб ви могли швидко співвіднести їх із кодом. Не намагайтеся завчити їх як мантру — краще тримайте як шпаргалку і звіряйте з тим, що реально відбувається в сервісному методі.
| Стан | Що це простими словами | ORM відстежує зміни? | Типовий спосіб отримати | Що зазвичай збиває новачка |
|---|---|---|---|---|
| transient | Звичайний новий Java-об’єкт, ORM про нього не знає | Ні | new Product() | Очікування, що setter → автоматичне збереження |
| managed | Об’єкт перебуває в поточному persistence context | Так | findById() у межах транзакції, нового об’єкта |
Сюрприз: «UPDATE відбувається сам» |
| detached | Об’єкт був managed, але контекст завершився або змінився | Ні | Повернули entity за межі транзакції, виконали |
Очікування, що зміни «потім якось збережуться» |
| removed | Об’єкт позначено на видалення в поточному контексті | Для звичайних змін це вже не головне | delete(entity) / у транзакції |
Очікування миттєвого фізичного видалення з БД |
Цього набору вже достатньо, щоб далі не плутати dirty checking, flush, commit і merge.
3. Що з цих чотирьох станів реально важливо просто зараз
transient — це звичайний new Product(). Поля можна змінювати скільки завгодно, але ORM поки що не має до цього жодного стосунку. Поки об’єкт не потрапив у контекст через save() / persist або каскад, він живе лише в JVM.
managed — ось тут починається вся сьогоднішня механіка. Лише для managed-сутності працюють identity map, кеш першого рівня і dirty checking. Саме тут можливий той самий «невидимий UPDATE», який пізніше лякає новачка.
detached — об’єкт був managed, але випав із поточного контексту. На простих полях це виглядає як «у Java все змінилося, а в БД — тиша». Цього факту нам поки що достатньо: detached більше не відстежується, і розраховувати на dirty checking як на автопілот уже не можна.
removed — об’єкт позначено на видалення всередині поточної unit of work. Це не обов’язково означає, що рядок фізично зникне в ту саму мілісекунду; це означає, що контекст уже веде цю сутність як таку, що видаляється.
Якщо хочеться однією фразою: уся подальша механіка тримається на межі між managed і всім іншим. Поки сутність managed, ORM може працювати разом із нею. Щойно ні — ви лишаєтеся сам на сам зі звичайним Java-об’єктом.
4. Переходи між станами без зайвої драматургії
У щоденному коді все зазвичай простіше, ніж може здатися з назв. Ви створили новий об’єкт — він transient. Завантажили його з репозиторію в межах транзакції або зберегли новий — він managed. Завершилася транзакція, виконали detach(...) або clear() — об’єкт став detached. Викликали delete(...) / remove(...) — для поточного контексту він removed.
stateDiagram-v2
[*] --> Transient: "new Product()"
Transient --> Managed: "save/persist або каскад"
Managed --> Detached: "кінець транзакції / detach / clear"
Managed --> Removed: "delete/remove"
Removed --> [*]: "транзакція завершилась"
Detached --> Managed: "шлях через save/merge"
Останній перехід легко зрозуміти неправильно. Повернення detached-об’єкта не робить старе посилання managed назавжди. JPA знову працює вже з об’єктом поточного контексту, і саме тому тут так легко заплутатися з посиланнями.
Ще один важливий захист від плутанини: стани entity — це не життєвий цикл рядка в таблиці. Життєвий цикл рядка — це INSERT, UPDATE, DELETE на рівні БД. А стани entity — це життєвий цикл об’єкта відносно поточного persistence context.
5. Шар даних Mini Shop: лише потрібний зріз
У placeOrder() картина зазвичай така. Product і StockItem, які ви завантажили всередині транзакції, — managed. Новий CustomerOrder і нові OrderItem, створені через new, — transient, доки ви не введете їх у контекст через save() або каскад. Якщо всередині тієї самої операції ви видаляєте сутність, для поточного контексту вона стає removed. А після завершення методу все, що ви тримаєте в руках зовні, уже detached.
Цього зрізу достатньо, щоб далі не гадати. Тут важливе одне запитання: об’єкт зараз managed чи ні? Якщо так, ORM може побачити його зміни. Якщо ні, ви просто змінюєте Java-об’єкт, і жодного «невидимого UPDATE» не відбудеться.
6. Типові помилки під час роботи зі станами entity
Помилки навколо станів — це не «ви погано вчилися», а нормальна плата за те, що ORM намагається бути зручним і тому приховує частину механіки. Стани — як коробка передач в автомобілі: поки їдете повільно й рівно, можна їх не помічати. Але варто повернути чи пришвидшитися — і раптом стає важливо, яка передача зараз увімкнена.
Помилка №1: «Якщо в об’єкта є id, значить він managed».
Це дуже популярна логіка, і вона майже завжди хибна. Наявність id говорить про те, що об’єкт схожий на сутність і, можливо, колись відповідав рядку в БД. Але managed — це про те, чи перебуває об’єкт у поточному persistence context. Об’єкт із id цілком може бути detached, а новий об’єкт може отримати id лише після збереження — але це не робить id головним маркером стану.
Помилка №2: «Я змінив об’єкт і очікував, що база оновиться сама».
Якщо об’єкт transient або detached, ви змінюєте просто Java-об’єкт. Він не «поганий», але не перебуває під контролем ORM. Якщо вам потрібно гарантоване збереження змін, працюйте в межах сервісної транзакції з managed-сутністю.
Помилка №3: «delete() видаляє рядок негайно, прямо в цю мілісекунду».
Видалення через JPA всередині транзакції спочатку переводить сутність у стан removed. Це частина unit of work: операцію заплановано, і вона має стати реальністю в момент синхронізації та завершення транзакції. Тому «миттєве зникнення» — не та модель, на яку варто спиратися в голові.
Помилка №4: «Я тримаю entity в полі сервісу/кеші та використовую її пізніше».
Сутність дуже легко стає detached і застаріває, тому що транзакції завершуються. Тримати entity як довготривалий стан — майже завжди погана ідея. Довготривало тримають ідентифікатори та прості значення, а сутності знову завантажують у новій unit of work.
Помилка №5: «Стани — це якась теорія, на практиці не потрібно».
На практиці це якраз те, що рятує від магічного мислення. Коли ви бачите дивну поведінку ORM, найпродуктивніше перше запитання звучить так: «Цей об’єкт зараз managed, detached чи removed?» Якщо ви можете на нього відповісти, половину розслідування вже зроблено.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ