JavaRush /Курси /Hibernate deep-dive /detach

detach ( )/ clear ( )/ remove ( )/ refresh ( )

Hibernate deep-dive
Рівень 2 , Лекція 4
Відкрита

1. Ручне керування EntityManager

Методи detach(), clear(), remove() і refresh() — це ручні важелі керування EntityManager. Якщо Hibernate — автопілот, то ці методи дають змогу точково втрутитися: від’єднати об’єкт, очистити контекст, позначити сутність на видалення або перечитати її з бази. У звичайному CRUD-коді вони потрібні не щодня, але щойно ви починаєте розуміти модель виконання, вони перестають бути загадковими заклинаннями зі Stack Overflow і стають звичайними інженерними інструментами.

Після persist(), find() і getReference() картина вже зрозуміла: сутності входять у контекст і починають жити за його правилами. Тепер потрібен другий бік тієї самої моделі — як цей зв’язок із контекстом явно змінити: від’єднати один об’єкт, очистити весь контекст, позначити сутність на видалення або перечитати її з бази.

Ключова думка проста: persistence context — це не абстракція заради краси, а «робочий блокнот» Hibernate. Поки об’єкт managed, Hibernate має право вважати його «своїм»: тримати в first-level cache, відстежувати його стан і планувати SQL-операції. А ці чотири методи дають змогу явно сказати Hibernate, що саме ви хочете зробити з об’єктом: перестати його вести, забути все одразу, позначити на видалення або викинути локальні зміни й перечитати стан із бази. Тут нам важливі саме переходи між станами і те, чи пов’язаний об’єкт із контекстом; цього вже достатньо, щоб перестати плутати ці методи з магічними кнопками save/delete.

Щоб усе було максимально наочно, ми постійно ставитимемо два контрольні запитання. Перше: «Сутність зараз managed чи ні?» Для цього чудово підходить entityManager.contains(entity). Друге: «Яка операція реально торкнулася бази?» Для цього ми дивимося на SQL trace. І саме тут починається інженерна частина, а не шаманство.

2. detach(): від’єднати сутність

detach() — це найточніший важіль. Він каже Hibernate: «ось саме цей об’єкт — більше не під твоїм керуванням». Не видаляй його з пам’яті — це взагалі не твоя справа, — не чіпай базу, просто перестань вважати його managed. На рівні моделі станів це класичний перехід managed → detached, який ми вже бачили на карті станів на початку дня.

Що змінюється після detach()

Якщо тримати в голові минулу лекцію про identity map, то detach() можна описати дуже просто: ви вийняли об’єкт із first-level cache поточного persistence context. Об’єкт і далі лежить у вашій змінній, його поля можна змінювати, можна викликати методи, можна друкувати його в лог. Але Hibernate перестає ставитися до нього як до «живого представника рядка».

Найпростіший спосіб переконатися в цьому й водночас виробити інженерну звичку — використовувати entityManager.contains(...).

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.commerce.catalog.entity.Product;

@Service
public class EntityStateControlLab {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void detachDemo(long productId) {
        // Завантажуємо сутність: після find() вона є managed-об’єктом у поточному persistence context
        Product product = entityManager.find(Product.class, productId);

        // contains() — швидкий спосіб перевірити: чи керує EntityManager цим об’єктом
        System.out.println(entityManager.contains(product)); // true

        // detach() "виймає" конкретний об’єкт із контексту (БД при цьому не чіпаємо)
        entityManager.detach(product);

        // Тепер EntityManager більше не відстежує зміни цього екземпляра
        System.out.println(entityManager.contains(product)); // false
    }
}

Тут важливий не сам друк, а думка: «я не довіряю відчуттям, я перевіряю факт». У deep-dive по Hibernate це майже девіз курсу.

detach() і identity map

Після detach() ви можете знову виконати find() за тим самим id — і отримаєте інший managed-об’єкт, тому що старий ви самі викинули з контексту.

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.transaction.annotation.Transactional;

import com.example.commerce.catalog.entity.Product;

@Transactional
public void detachAndFindAgain(long productId) {
    // Перший find(): отримуємо managed-екземпляр, який лежить в identity map контексту
    Product p1 = entityManager.find(Product.class, productId);

    // Самі "відв’язуємо" цей екземпляр від контексту
    entityManager.detach(p1);

    // Другий find(): контекст уже не знає p1, тож створить новий managed-екземпляр
    Product p2 = entityManager.find(Product.class, productId);

    // Посилання різні: це різні Java-об’єкти, хоча id один і той самий
    System.out.println(p1 == p2); // false
}

Це хороший момент для перевірки інтуїції: один і той самий id не гарантує, що у вас одне й те саме Java-посилання, якщо ви самі розірвали зв’язок із контекстом.

Коли detach() справді буває корисним

У межах курсу нам корисно бачити detach() як навчальний мікроскоп: він допомагає відчути межу між «об’єкт живе в пам’яті» і «об’єкт живе під керуванням ORM».

У реальних застосунках detach() трапляється рідше, ніж clear(), але буває доречним у складних сценаріях, коли ви хочете гарантовано сказати: «цей об’єкт більше не бере участі в поточному unit of work». Це може бути важливо і для передбачуваності, і для пам’яті, і для налагодження. Але якщо ви почнете вставляти detach() у кожен сервіс «про всяк випадок», код швидко стане схожим на літак, де пілот кожні 30 секунд вимикає систему стабілізації, тому що «так надійніше».

3. clear(): очистити persistence context

clear() — це вже «ядерна кнопка» порівняно з detach(). Якщо detach() вириває один об’єкт, то clear() каже: «Hibernate, забудь узагалі все, що ти зараз ведеш». Тобто весь persistence context очищується, first-level cache стає порожнім, а всі раніше managed-сутності в цьому контексті фактично перетворюються на detached — з точки зору зв’язку з поточним EntityManager.

Психологічно clear() зручно уявляти як «перезапуск пам’яті» для ORM усередині поточної транзакції. Але з важливою застережною ремаркою: ваші Java-посилання нікуди не зникають. І саме тут починаються найтиповіші помилки: об’єкт лежить у змінній, ви його бачите, але Hibernate його більше не «тримає». Це як знайомий у телефоні: контакт є, але ви видалили його з адресної книги — зателефонувати можна, а книга вже не підтверджує, що це та сама людина.

Спостерігаємо ефект clear() через == і contains()

Давайте проведемо невеликий експеримент на Product із нашого Commerce Persistence Lab. Він чудово показує, що clear() скидає identity map.

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.transaction.annotation.Transactional;

import com.example.commerce.catalog.entity.Product;

@Transactional
public void clearDemo(long productId) {
    // Завантажуємо сутність: вона managed і знаходиться в first-level cache поточного контексту
    Product p1 = entityManager.find(Product.class, productId);

    // clear() очищає весь persistence context: усі managed-сутності стануть detached
    entityManager.clear(); // контекст порожній

    // Повторний find() поверне новий managed-екземпляр (identity map уже скинуто)
    Product p2 = entityManager.find(Product.class, productId);

    // Java-посилання різні — це два різні об’єкти
    System.out.println(p1 == p2);                   // false

    // p1 більше не під керуванням EntityManager
    System.out.println(entityManager.contains(p1)); // false

    // p2 — свіжий managed-об’єкт
    System.out.println(entityManager.contains(p2)); // true
}

Це один із найчесніших способів «помацати» first-level cache руками. З точки зору БД ви читаєте один і той самий рядок. З точки зору Java — у вас два різні об’єкти. І це не «помилка Hibernate», а прямий результат того, що ви самі очистили контекст.

Ризики clear()

Головна пастка clear() навіть не в тому, що він «обнуляє кеш», а в тому, що він робить ваш поточний об’єктний граф раптово некерованим. Якщо ви десь далі в коді продовжите працювати з уже завантаженими сутностями як із managed — наприклад, очікуючи, що ORM «сама все побачить», — то отримаєте поведінку в дусі «воно не збереглося» або «метод упав», і будете сварити Hibernate. Хоча сварити доведеться… себе вчорашнього, який поставив clear() посеред сценарію.

У цій лекції clear() потрібен насамперед як інструмент розуміння: показати межу контексту і роль identity map. Але корисно одразу запам’ятати й практичний сенс: після операцій, які обходять звичну managed-модель, неочищений контекст легко починає тримати застарілі дані.

4. remove(): позначити сутність на видалення

remove() звучить як «видалити об’єкт», і новачка ця назва легко збиває. Мозок одразу малює картинку: об’єкт зник із пам’яті, Java-посилання стало null, і взагалі все стерлося. Реальність набагато прозаїчніша: remove() працює всередині persistence context, і його сенс — перевести сутність зі стану managed у стан removed, тобто позначити її на видалення з бази під час синхронізації.

І так, як і з persist(), тут важливо не плутати «я викликав метод» і «SQL уже пішов». Зазвичай DELETE відправиться в базу на коміті транзакції або під час явної синхронізації, а remove() — це саме зміна стану в контексті й планування майбутнього DELETE.

remove() вимагає managed-об’єкт

За правилами JPA, remove() очікує, що сутність перебуває під керуванням поточного EntityManager. Тому типовий і найзрозуміліший сценарій виглядає так: find()remove().

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.transaction.annotation.Transactional;

import com.example.commerce.catalog.entity.Product;

@Transactional
public void removeDemo(long productId) {
    // Спочатку отримуємо managed-екземпляр у поточному EntityManager
    Product product = entityManager.find(Product.class, productId);

    // remove() позначає сутність як removed (DELETE піде пізніше, під час синхронізації/коміту)
    entityManager.remove(product);
}

Тут важливіше не те, що покаже допоміжна перевірка на кшталт contains(), а сам сенс стану: об’єкт уже позначений на видалення всередині поточного unit of work. Він і далі може існувати як Java-посилання, але бізнес-логіка не повинна ставитися до нього як до звичайної живої сутності.

remove() для detached-сутності

Якщо ви спробуєте видалити detached-сутність — наприклад, завантажили її в іншій транзакції, вийшли з неї, а потім принесли об’єкт у нову, — JPA зазвичай не в захваті. Найчастіше ви отримаєте IllegalArgumentException, тому що EntityManager не керує цим екземпляром.

Правильна звичка для початківця звучить нудно, зате рятує години життя: якщо хочете видалити — отримайте managed-екземпляр у поточній транзакції, а потім видаляйте.

У нашому проєкті це виглядатиме приблизно так: «у сервісі завантажити Product за id і видалити його». Не треба намагатися «економити» на find(), доки ви не розумієте всіх побічних ефектів. Економія на одному SELECT іноді перетворюється на дві години розбору «а чому воно впало на видаленні».

Що важливо пам’ятати після remove() в тому самому контексті

Після remove() не варто будувати логіку на допоміжних перевірках на кшталт повторного find() за тим самим id. Конкретні деталі такої поведінки краще не робити своєю опорою. Надійне правило простіше: в межах поточного unit of work сутність уже позначена на видалення, і далі код має виходити саме з цього.

5. refresh(): перечитати з бази

Після detach() і clear() потрібен симетричний важіль: інколи об’єкт не хочеться від’єднувати або видаляти, а навпаки — потрібно викинути локальні зміни й знову взяти стан із бази.

refresh() — метод, який найчастіше або не знають, або знають і бояться чіпати, і загалом це зрозуміло. Він потрібен, коли ви хочете сказати Hibernate: «візьми поточну версію даних із бази й поклади її назад у мій managed-об’єкт, скасувавши локальні зміни». Це не про «прискорити кеш», а про консистентність: ви визнаєте, що стан об’єкта в пам’яті вам більше не підходить, і просите ORM примусово синхронізуватися з тим, що лежить у таблиці.

Важливо одразу зафіксувати дві речі. По-перше, refresh() працює тільки для managed сутності. По-друге, refresh() майже неминуче робить SELECT, тому що йому потрібно перечитати дані. Отже, refresh() — це свідомий і доволі «дорогий» важіль, який варто застосовувати рідко, але розуміння його поведінки дуже зміцнює вашу модель того, що відбувається.

Демонстрація: «передумав — поверни як у базі»

Уявімо, ви завантажили товар, змінили ім’я — випадково або спеціально, — а потім вирішили: «ні, хочу назад оригінал».

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.transaction.annotation.Transactional;

import com.example.commerce.catalog.entity.Product;

@Transactional
public void refreshDemo(long productId) {
    // Отримуємо managed-об’єкт: refresh() працює тільки з такими сутностями
    Product product = entityManager.find(Product.class, productId);

    // Робимо локальну зміну в пам’яті (ще не факт, що вона пішла в БД)
    product.setName("Тимчасове імʼя");

    // refresh() перезапише поля значеннями з БД, "скасувавши" локальні зміни
    entityManager.refresh(product);

    // Після refresh() ім’я знову буде як у базі
    System.out.println(product.getName()); // ім’я знову як у базі
}

Якщо у вас увімкнено SQL trace, ви побачите SELECT ... FROM product WHERE id=? або щось дуже близьке. І це нормально: refresh() — не телепатія, йому потрібно реально сходити в базу.

Коли refresh() буває доречним у реальному житті

Найпростіший кейс: ви зробили зміни в об’єкті в межах транзакції, але потім вирішили їх скасувати — не створюючи нову сутність, не копіюючи поля назад вручну і не влаштовуючи собі «undo» через власний код.

Більш прикладний і рідкісніший кейс: база могла змінити дані сама, наприклад через тригери, або паралельна транзакція вже закомітила зміни, а ви хочете побачити свіжі значення в поточному контексті. Тут потрібно бути дуже обережним: refresh() — не заміна правильному проєктуванню транзакційних меж. Але як інструмент діагностики і як «скидання в стан із бази» він корисний.

refresh() і detached: дружби не буде

Якщо об’єкт detached, то refresh() зазвичай кидає виняток, тому що EntityManager не має права «перезаписати» об’єкт, який він не веде. І це логічно: інакше ви могли б оновлювати що завгодно, не маючи зв’язку з контекстом.

Тому в голові зручно тримати просте правило: refresh() — це операція над managed-сутністю, так само як remove(). А detach() і clear() — це операції «зняти керування».

6. Шпаргалка по detach/clear/remove/refresh

До цього моменту в голові легко може почати утворюватися легка каша: «detach ніби від’єднує, clear ніби теж, remove щось видаляє, refresh щось перечитує…». Щоб мозок не перегрівався, корисно тримати поруч компактну шпаргалку. Вона не замінює розуміння, але чудово допомагає швидко перевірити свою гіпотезу перед тим, як робити висновки щодо поведінки коду.

Таблиця порівняння

Метод На що впливає Потрібен managed? Чи змінює БД одразу? Що відбувається з об’єктом у пам’яті
detach(entity) один об’єкт так (за змістом) ні об’єкт залишається, але стає detached
clear() увесь persistence context ні (застосовно завжди) ні усі об’єкти, які були managed, стають detached
remove(entity) один об’єкт так зазвичай ні (до синхронізації) об’єкт залишається, але стає removed
refresh(entity) один об’єкт так робить SELECT об’єкт залишається managed, але поля перезаписуються даними з БД

Мінісхема переходів станів

stateDiagram-v2
    Transient --> Managed: "persist()"
    Managed --> Detached: "detach()"
    Managed --> Detached: "clear() для всіх"
    Managed --> Removed: "remove()"
    Managed --> Managed: "refresh()"

    %% фізичне видалення/вставка відбуваються під час синхронізації з БД

Зверніть увагу: на схемі немає пункту «видалити об’єкт із пам’яті». Тому що JPA/Hibernate цим не займаються. І це хороший момент, щоб ще раз відокремити дві реальності. Java керує пам’яттю — і збирач сміття вирішує, коли об’єкт зникне. Hibernate керує зв’язком об’єкта з БД через persistence context.

7. Типові помилки методів EntityManager

Помилка №1: очікування, що remove() «видалить об’єкт із пам’яті».
Початківець часто підсвідомо думає: «я ж викликав remove — значить об’єкта більше немає». Потім дивується, що змінна й далі вказує на об’єкт, методи викликаються, поля читаються. Це нормально: remove() — про стан усередині persistence context, а не про знищення Java-об’єкта. Якщо потрібно позбутися посилання в коді, це вже ваша відповідальність, але до ORM це не має стосунку.

Помилка №2: виклик remove() або refresh() на detached-сутності.
Дуже частий сценарій: об’єкт завантажили в одній транзакції, вийшли з неї, потім принесли в інший метод і намагаються «просто видалити» або «просто refresh». Але EntityManager не керує цим екземпляром, тому поведінка буде або винятком, або несподіванкою. Правильна звичка — у межах поточної транзакції отримати managed-екземпляр, зазвичай через find(), і вже з ним робити remove() або refresh().

Помилка №3: використовувати clear() як «кнопку полагодити все».
clear() інколи сприймають як універсальне «скидання» і ставлять туди, де «щось дивно працює». На короткій дистанції це може заспокоїти поведінку, а на довгій — створює хаос: у вас залишаються посилання на об’єкти, які ви продовжуєте змінювати, але ORM їх більше не веде. Виходить ефект «ніби змінював — не збереглося», і починається полювання на відьом. clear() корисний, коли ви розумієте, що робите, а не коли ви просто втомилися.

Помилка №4: плутанина між detach() і «не викликати репозиторій save()».
Інтуїтивно хочеться думати: «якщо я не викликав save(), значить зміни не підуть». Але це мислення зі світу ручного SQL. В ORM важливий стан об’єкта: managed він чи detached. detach() — це явний спосіб перевести об’єкт у detached. Відсутність виклику save() сама по собі не гарантує «нічого не станеться», якщо об’єкт залишається managed. Зараз достатньо запам’ятати принцип на рівні моделі станів: поки об’єкт залишається managed, відсутність виклику save() сама по собі нічого не гарантує.

Помилка №5: використовувати refresh() як повсякденну «синхронізацію про всяк випадок».
Іноді розробник починає викликати refresh() після кожної зміни, щоб «бути впевненим». Це перетворює застосунок на генератор зайвих SELECT і робить код важкочитабельним. refresh() — це осмислений важіль «поверни як у базі», а не щоденна гігієна. Якщо вам постійно потрібно «перечитувати з бази», зазвичай проблема не у відсутності refresh(), а в тому, як спроєктовано операцію та її межі.

1
Задача
Hibernate deep-dive, 2 рівень, 4 лекція
Недоступна
Різниця між `detach()` і `clear()`
Різниця між `detach()` і `clear()`
1
Задача
Hibernate deep-dive, 2 рівень, 4 лекція
Недоступна
`refresh()` і `remove()` для двох окремих товарів
`refresh()` і `remove()` для двох окремих товарів
1
Опитування
Hibernate Entity, рівень 2, лекція 4
Недоступний
Hibernate Entity
Стан і контекст
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ