Явний flush ( ) як інструмент спостереження

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

1. Роль явного flush()

Явний flush() насамперед потрібен як контрольна точка синхронізації. Він дає змогу не чекати, доки Hibernate сам вибере момент. Для звичайної бізнес-операції це найчастіше не потрібно, але в діагностиці без такої точки швидко виникає хаос: незрозуміло, який саме блок коду згенерував SQL, де спрацювало обмеження БД і чи ви вже читаєте базу, чи ще дивитеся на ту саму керовану копію.

Явний flush() — це не кнопка «зробити все надійнішим» і не заміна commit. Це інструмент спостереження та діагностики: ви явно кажете Hibernate «синхронізуй контекст саме тут, щоб я побачив SQL, упіймав помилку БД ближче до місця проблеми або чесно перечитав стан із бази».

flush(): памʼять, БД і commit

Тут достатньо однієї простої формули: flush() надсилає накопичені INSERT/UPDATE/DELETE до БД в межах поточної транзакції, а commit завершує транзакцію і остаточно фіксує зміни. Тому після flush() SQL уже видно, помилку обмеження вже можна спіймати, але rollback усе ще може скасувати результат.

Цієї різниці досить, щоб не плутати діагностичний flush() із фінальною фіксацією транзакції. Далі якраз стане в пригоді ця межа: flush()+clear() допомагає чесно читати БД, а явний flush() — ловити помилки раніше.

2. Зв’язка flush() + clear() для чесного читання БД

Якщо ви хочете перевірити, що «в базі справді так», є тонка пастка: всередині persistence context Hibernate намагається бути корисним і економним. Якщо ви двічі викликаєте find(Product.class, 1L) в одній транзакції, вдруге SQL може взагалі не піти — ви отримаєте той самий керований об’єкт із кешу першого рівня. І це правильно: ORM захищає identity map і гарантує «один рядок — один об’єкт».

Тому якщо ви хочете свідомо побачити різницю між «у мене в памʼяті нове імʼя» та «у мене в базі нове імʼя», часто потрібна така послідовність: спочатку flush(), потім clear(), потім повторне читання. Сенс простий: flush() надсилає зміни до БД, а clear() викидає керовану копію, щоб наступний find() справді звернувся до бази.

Мінісценарій на Product усередині транзакції може виглядати так:

import jakarta.persistence.EntityManager;

// 1) Читаємо сутність: вона стає керованою в поточному persistence context
Product p = entityManager.find(Product.class, 1L);

// 2) Змінюємо поле: поки це лише зміна в памʼяті (Hibernate позначає об’єкт як dirty)
p.setName("Phone Pro");

// 3) Явно протискаємо накопичені зміни в SQL просто зараз
entityManager.flush();                   // у SQL-журналі зʼявляється UPDATE

// 4) Викидаємо керовані об’єкти з persistence context,
//    щоб наступне читання точно не повернуло ту саму Java-посилання з кешу першого рівня
entityManager.clear();

// 5) Тепер це буде повторне читання з БД (SELECT), а не повернення з кешу першого рівня
Product reread = entityManager.find(Product.class, 1L);

І важлива деталь, яку легко пропустити: змінна p після clear() нікуди не зникає. Це все той самий Java-об’єкт, але для поточного persistence context він уже detached. Якщо після clear() ви й далі викликатимете p.setName(...), Hibernate цього не побачить: dirty checking працює лише з керованими сутностями, доки ви не перечитаєте об’єкт або не повернете його в контекст.

Тут є важливий психологічний ефект: після clear() ви починаєте довіряти лише тому, що можна знову прочитати з бази. А це дуже корисна звичка, коли ви налагоджуєте, чому запити поводяться дивно або чому в журналі зʼявився несподіваний UPDATE.

У нашому Commerce Persistence Lab таку зв’язку зручно використовувати в невеликих лабораторних сценаріях — наприклад, у компоненті, який запускається разом із застосунком у dev-профілі і виконує один зрозумілий експеримент, поки ви дивитеся на SQL-трейс. Але навіть без спеціальних раннерів це чудовий прийом для розбору в дебагері.

3. clear() без flush(): як «стерти» зміни

Якщо зв’язка flush() + clear() допомагає чесно перечитати стан із БД, то зворотна комбінація clear() без flush() — це спосіб зробити вигляд, що змін ніколи не було. Іноді це корисно (наприклад, якщо ви свідомо хочете викинути всі накопичені керовані зміни), але частіше це пастка: ви робите clear() «щоб перечитати», а потім дивуєтеся, чому база «не оновилася».

Уявіть, що ви змінили керовану сутність, але ще не відбувся ні явний, ні автоматичний flush. Зміна живе лише в persistence context. Якщо ви робите clear(), ви викидаєте цей контекст. Hibernate більше не зобов’язаний пам’ятати про вашу зміну, бо ви самі попросили його «все забути». А база, звісно, нічого не дізналася, бо SQL так і не був надісланий.

Ось короткий приклад, який корисно пам’ятати як антизаклинання:

import jakarta.persistence.EntityManager;

// 1) Сутність керована
Product p = entityManager.find(Product.class, 1L);

// 2) Зміна лише в памʼяті (ще не синхронізовано з БД)
p.setName("Чернеткове імʼя");

// 3) Скидаємо persistence context: Hibernate забуває про накопичені зміни
entityManager.clear();

// 4) Повторно читаємо з бази: отримуємо старе значення, бо SQL UPDATE не надсилався
Product reread = entityManager.find(Product.class, 1L);

У результаті reread.getName() поверне старе імʼя з БД, і це буде цілком чесно. Жодної містики: ви не «втратили дані», ви втратили несинхронізований стан persistence context.

Окремо підкреслю: clear() — це операція не про базу, а про поточний контекст. Якщо сприймати її як «перезавантажити дані з бази», то майже напевно ви зробите неправильні висновки. Для «перезавантажити» є інший інструмент — refresh(), а для «протиснути зміни» — flush().

4. Раннє виявлення помилок БД через flush()

Тепер найпрактичніша причина, чому flush() люблять у діагностиці: він змушує SQL піти в базу просто зараз, а отже, база може просто зараз перевірити обмеження. Це корисно, коли сервісний метод довгий, робить багато кроків, і ви не хочете дізнатися про помилку унікальності або NOT NULL лише наприкінці, на commit, коли «все вже сталося» й місце помилки розмивається.

У нашому проєкті є щонайменше один очевидний кандидат: унікальність Product.sku. Якщо ви випадково створите два товари з однаковим SKU в межах однієї транзакції, то до flush усе може виглядати «нормально»: у вас два Java-об’єкти, обидва керовані, обидва коректні. Але PostgreSQL у певний момент скаже: «Стоп, порушено унікальність».

З явним flush це можна побачити ближче до місця, де ви створили проблему:

import jakarta.persistence.EntityManager;

// 1) Створюємо і зберігаємо перший товар у persistence context
Product a = new Product();
a.setSku("SKU-123");
entityManager.persist(a); // SQL INSERT може бути відкладено до flush/commit

// 2) Створюємо другий товар з тим самим SKU (проблема ще не зобов'язана проявитися відразу)
Product b = new Product();
b.setSku("SKU-123");
entityManager.persist(b);

// 3) Примусово надсилаємо SQL до БД: тут база перевірить UNIQUE і може впасти
entityManager.flush(); // тут імовірне порушення обмеження

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

Є ще один важливий нюанс: якщо помилка виникає на flush, це все одно не означає, що частина даних «встигла зберегтися». У більшості випадків транзакцію буде позначено як rollback-only, і все відкочиться. Але для діагностики ви виграєте головне: ловите проблему не «на виході з методу», а майже в точці, де її створено.

5. Перевірка через запит до БД після flush()

Іноді в навчальних і діагностичних сценаріях хочеться зробити щось на кшталт: «Я змінив імʼя товару, а тепер прямо в межах цієї ж транзакції хочу перевірити, що в таблиці product уже лежить нове імʼя». Так, Hibernate в AUTO-режимі часто й так зробить flush перед запитом, який може побачити ці зміни, але спиратися на «часто» — поганий стиль у діагностиці. Якщо вам важливо, щоб SQL точно вже пішов, робіть це явно і читайте результат спокійніше.

Наприклад, ви хочете після зміни імені перевірити, скільки рядків уже відповідає новому значенню:

import jakarta.persistence.EntityManager;

// 1) Читаємо сутність і змінюємо її в памʼяті
Product p = entityManager.find(Product.class, 1L);
p.setName("Phone Pro");

// 2) Гарантуємо, що UPDATE уже надіслано до БД (у межах поточної транзакції)
entityManager.flush();

// 3) Робимо запит напряму до таблиці, щоб «запитати базу»,
//    а не отримати значення з persistence context
Number count = (Number) entityManager
        .createNativeQuery("""
                select count(*)
                from product
                where name = 'Phone Pro'
                """)
        .getSingleResult();

// 4) У межах транзакції PostgreSQL бачить зміни після flush
System.out.println("кількість = " + count); // очікуємо, наприклад: кількість = 1

Тут ми спеціально використовуємо native SQL, щоб «запитати напряму таблицю», без участі persistence context. У межах транзакції PostgreSQL бачить уже надіслані зміни, тому результат запиту має відповідати тому, що ви щойно записали (знову ж таки: доки транзакцію не відкочено).

Це хороший прийом, коли ви досліджуєте поведінку режимів flush. Наприклад, якщо ви перемкнули flush mode на COMMIT або MANUAL, то JPQL-запит, який інакше побачив би зміни, може поводитися по-іншому, і вам потрібно зробити поведінку максимально явною: «спочатку flush, потім запит». У таких експериментах flush() — це як яскрава лампа в темній кімнаті: вона не лагодить меблі, але дає змогу нарешті побачити, що саме у вас стоїть криво.

6. Доречний і зайвий flush()

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

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

Дуже простий маркер здорового коду: у звичайному сервісному методі ви найчастіше не зобов’язані вручну викликати flush(). Hibernate сам зробить flush перед commit і в потрібні моменти для коректності запитів (залежно від режиму flush). А от у коді, що досліджує поведінку ORM (наш курс саме про це), flush() — чудовий перемикач фаз, який робить експерименти відтворюваними.

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

7. Типові помилки під час роботи з flush()

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

Помилка №1: сприймати flush() як commit.
Якщо після flush() ви бачите SQL у журналі, легко вирішити, що «дані вже збережено назавжди». Насправді flush() просто надіслав команди до БД, але транзакція ще може відкочитися. Це особливо помітно, коли наприкінці методу ви кидаєте виняток і бачите, що в базі нічого не змінилося — і це правильно.

Помилка №2: робити «перевірку з бази» без clear() і вірити результату.
Часта сцена: розробник змінив сутність, викликав find() ще раз, побачив нове значення і вирішив, що база оновилася. Але в межах однієї транзакції find() може повернути той самий керований об’єкт із кешу першого рівня. Якщо ви хочете чесно перечитати з БД, вам потрібен clear() (або інший спосіб викинути керовану копію) перед повторним читанням.

Помилка №3: викликати clear() до flush() і втрачати несинхронізовані зміни.
Це тонка, але дуже дорога помилка, бо вона не завжди призводить до винятку — іноді вона просто «з’їдає» вашу роботу. Зміни в керованих об’єктах жили лише в persistence context. Ви зробили clear() — і самі попросили Hibernate забути їх. База при цьому нічого не отримала, бо SQL не надсилався.

Помилка №4: перетворювати flush() на ритуал «після кожного виклику setter».
Такий код зазвичай з’являється зі страху: «раптом Hibernate не збереже». Але Hibernate якраз для цього і існує: зберігати автоматично наприкінці unit of work. Постійний ручний flush робить виконання важчим, ускладнює налагодження порядку SQL-операцій і може призводити до ранніх помилок обмежень там, де вони мали виникнути лише на фінальній фіксації бізнес-операції.

Помилка №5: робити висновки без SQL-логу і «вгадувати» момент flush.
flush() — це про спостережуваність. Якщо ви не дивитеся на SQL trace (або хоча б на статистику Hibernate), ви думатимете, що flush був «тут», а він був «там», і вся діагностика поїде. У нашому курсі правильна звичка така: будь-яку розмову про flush підтверджуйте тим, що реально пішло в PostgreSQL.

1
Опитування
Hibernate Flush, рівень 4, лекція 4
Недоступний
Hibernate Flush
Синхронізація з базою даних
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ