Що робить метод refresh()
Метод refresh() інтерфейсу EntityManager примусово перезавантажує стан керованої сутності (Managed) безпосередньо з реляційної бази даних. Він синхронізує поля Java-об'єкта в оп...
Метод refresh() інтерфейсу EntityManager примусово перезавантажує стан керованої сутності (Managed) безпосередньо з реляційної бази даних. Він синхронізує поля Java-об’єкта в оперативній пам’яті JVM з рядком таблиці СУБД, безповоротно перезаписуючи будь-які локальні незбережені зміни та оновлюючи знімок стану в кеші першого рівня L1.
🟢 Junior Level
Призначення та базова поведінка refresh()
За замовчуванням Hibernate покладається на кеш першого рівня: якщо сутність уже завантажена в сесію, повторний виклик find() не звертається до БД. Метод refresh() ігнорує кеш L1, примусово виконує фізичний SQL SELECT за первинним ключем і затирає поточні поля об’єкта актуальними даними з бази:
@Transactional
public void refreshDemo() {
User user = entityManager.find(User.class, 1L); // user.getName() == "Іван"
// 1. Змінюємо поле в оперативній пам'яті
user.setName("Тимчасове ім'я");
// 2. Викликаємо refresh()
entityManager.refresh(user);
// Hibernate виконує: SELECT * FROM users WHERE id = 1;
// 3. Локальні зміни стерті!
System.out.println(user.getName()); // Знову виведе "Іван"!
// При коміті транзакції SQL UPDATE згенеровано НЕ БУДЕ.
}
Головні правила виклику refresh()
- Працює лише для сутностей зі статусом
Managed: Якщо передати об’єкт у станіDetachedабоTransient, Hibernate негайно викине виняток:java.lang.IllegalArgumentException: Entity not managed. - Затирає незбережені дані: Усі зміни полів об’єкта, зроблені в Java-коді до виклику
refresh(), безповоротно втрачаються (якщо попередньо не було виконаноflush()). - Оновлює знімок Dirty Checking: Знімок полів (
loadedState) всередині L1 кешу оновлюється новими значеннями з БД.
СХЕМА РОБОТИ МЕТОДУ REFRESH()
[ Java Heap: User@0x1 ] [ База даних: users ]
name = "Змінено в пам'яті" id = 1, name = "Іван у БД"
│ │
│──── 1. entityManager.refresh(user) ─│
│ │
│<─── 2. SQL SELECT * WHERE id = 1 ───│
│ (Читання актуального рядка) │
▼ ▼
[ Java Heap: User@0x1 ] [ База даних: users ]
name = "Іван у БД" id = 1, name = "Іван у БД"
(Snapshot оновлено, UPDATE скасовано)
🟡 Middle Level
Коли refresh() дійсно необхідний у Production
- Робота тригерів бази даних:
Якщо в СУБД налаштовані тригери (
BEFORE INSERT/BEFORE UPDATE), які обчислюють колонки (наприклад, генерують промокод, статус або рахують підсумкову суму), Hibernate не знає про ці зміни.entityManager.persist(order); entityManager.flush(); // Фізично відправляє INSERT, тригер БД спрацьовує entityManager.refresh(order); // Завантажує обчислені тригером поля - Отримання колонок зі значеннями за замовчуванням (
DEFAULT): Коли колонка ініціалізується базою даних (created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP), викликrefresh()підтягує згенеровані СУБД таймстемпи. - Після масових оновлень (
Bulk Update/@Modifying): Пакетний запитUPDATE User u SET u.active = falseвиконується безпосередньо в базі даних, оминаючи L1 кеш. Раніше завантажені в сесію сутностіUserмістять застарілі поля. Викликrefresh()повертає їм актуальний стан.
refresh() із накладенням блокувань (LockModeType)
Метод refresh() дозволяє не просто перечитати дані, але й накласти на рядок суворе блокування в СУБД у межах єдиного SQL-запиту:
// Блокування на запис: SELECT ... FOR UPDATE
entityManager.refresh(account, LockModeType.PESSIMISTIC_WRITE);
// Блокування з таймаутом очікування звільнення рядка
Map<String, Object> hints = Map.of("jakarta.persistence.lock.timeout", 3000); // 3 секунди
entityManager.refresh(account, LockModeType.PESSIMISTIC_WRITE, hints);
Каскадний refresh (CascadeType.REFRESH)
За замовчуванням refresh(order) перезавантажує лише поля самої сутності Order. Пов’язані колекції та залежності не оновлюються. Щоб оновити весь граф залежностей, потрібен каскад:
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.REFRESH)
private List<OrderItem> items = new ArrayList<>();
}
При виклику entityManager.refresh(order) Hibernate виконає каскадний refresh() для кожного дочірнього елемента OrderItem.
🔴 Senior Level
Внутрішній механізм Hibernate: Що відбувається зі StatefulPersistenceContext
Всередині ядра Hibernate (SessionImpl) виклик refresh() обробляється слухачем DefaultRefreshEventListener:
- Перевіряється статус в
entityEntries. Якщо сутність неStatus.MANAGED, викидаєтьсяIllegalArgumentException. - Якщо в
ActionQueueбули накопичені незафіксовані дії над цією сутністю, вони можуть конфліктувати зі скиданням. - Hibernate формує SQL
SELECTз усіма змаппленими колонками та виконує його через JDBC. - Значення колонок витягуються з
ResultSetі записуються в поля цільового Java-об’єкта (через прямий доступ до полів заField.set()або сетери). - Критичний крок: Масив
loadedStateвсерединіEntityEntryповністю перезаписується новими значеннями. Це гарантує, що наступний запуск Dirty Checking порахує сутність абсолютно «чистою» (not dirty). - Поведінка колекцій (
PersistentCollection):- Якщо колекція не була ініціалізована (
uninitialized), вона залишається у вигляді лінивого проксі. - Якщо колекція вже була завантажена в пам’ять, Hibernate очищає її елементи та повторно ініціалізує свіжими рядками з БД.
- Якщо колекція не була ініціалізована (
Сучасна альтернатива: @Generated в Hibernate 6
У сучасних версіях Hibernate ручний виклик refresh() заради отримання дефолтних колонок або значень тригерів застарів. Рекомендується використовувати анотацію @Generated:
@Entity
public class Invoice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Hibernate 6 сам підтягне згенероване СУБД значення через RETURNING / додатковий SELECT
@Generated(event = EventType.INSERT)
@Column(name = "created_at", insertable = false, updatable = false)
private Instant createdAt;
@Generated(event = {EventType.INSERT, EventType.UPDATE})
@Column(name = "total_amount", insertable = false, updatable = false)
private BigDecimal totalAmount;
}
При використанні @Generated Hibernate автоматично генерує INSERT ... RETURNING created_at, total_amount (у PostgreSQL) або читає згенеровані значення одразу після збереження, усуваючи необхідність у ручному flush() + refresh().
4 Tricky Questions
1. Що станеться, якщо змінити поле сутності user.setEmail("new@mail.com"), потім викликати entityManager.refresh(user), а в кінці транзакції завалідувати/закомітити зміни? Чи буде відправлено SQL UPDATE?
Відповідь:
- SQL
UPDATEвідправлено не буде. - Метод
refresh()виконує негайний SQLSELECT, читає старий email з БД і перезаписує поле об’єкта в пам’яті JVM. - Одночасно з цим Hibernate оновлює знімок
loadedStateу контекстіPersistenceContext. - Наприкінці транзакції Dirty Checking порівнює поточне поле об’єкта (яке містить старий email) зі знімком (який також містить старий email). Відмінностей немає — жодних
UPDATEне планується.
2. Що станеться, якщо викликати entityManager.refresh(user), але рядок з цим ID було видалено в базі даних паралельною транзакцією?
Відповідь:
- Метод
refresh()виконаєSELECT * FROM users WHERE id = ?. - База даних поверне порожній
ResultSet(0 рядків). - Специфікація JPA вимагає в такому випадку негайно викинути виняток:
jakarta.persistence.EntityNotFoundException: Entity no longer exists in the database. - Транзакція буде переведена в режим
Rollback-Only.
3. Ми виконали запит у Spring Data JPA: @Modifying @Query("UPDATE User u SET u.active = false"). Чому без виклику refresh() або спеціального параметра анотації ми побачимо старі дані?
Відповідь:
- Анотація
@Modifyingвиконує прямий SQL/JPQLUPDATEна рівні СУБД, повністю оминаючиPersistenceContext(кеш L1). - Об’єкти
User, що вже знаходяться в пам’яті поточної сесії, нічого не знають про цю зміну і продовжують зберігатиactive = true. - Щоб виправити розсинхронізацію, необхідно або викликати
entityManager.refresh(user)для кожної сутності, або використовувати параметр анотації@Modifying(clearAutomatically = true). ПараметрclearAutomatically = trueочистить весь кеш L1, змусивши наступнийfind()прочитати свіжі дані з бази.
4. Чи може виклик refresh() призвести до NonUniqueObjectException, якщо в сесії є пов’язані ліниві сутності?
Відповідь:
- Ні, для самої оновлюваної сутності це виключено завдяки Identity Map: оновлюється саме існуючий екземпляр у пам’яті за посиланням.
- Проте, якщо під час каскадного
refresh(order)перезавантажується колекція, що містить дочірні елементи, які раніше були від’єднані (Detached) і заново створені в паралельному коді без участі сесії, спроба повторної прив’язки дублюючого екземпляра може призвести до конфлікту ідентичності вPersistenceContext.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«Метод
entityManager.refresh()примусово перезавантажує станManagedсутності з бази даних, виконуючи SQLSELECTза первинним ключем. Він затирає будь-які незбережені локальні зміни в полях об’єкта та оновлює знімокloadedStateдля Dirty Checking. Основні сценарії застосування: підтягування значень, згенерованих тригерами БД або значеннямиDEFAULT, синхронізація після нативних@Modifyingзапитів та блокування рядків черезrefresh(entity, LockModeType.PESSIMISTIC_WRITE). У сучасному Hibernate 6 необхідність уrefresh()для тригерів знизилася завдяки анотації@Generated».
Ключові метрики для Production-моніторингу
- Кількість SELECT-запитів на один HTTP-запит: часті виклики
refresh()створюють зайвий мережевий оверхед на повторне читання вже наявних даних. - EntityNotFoundException rate: сплеск помилок вказує на видалення рядків конкурентними процесами перед спробою синхронізації.
Червоні прапорці (Чого говорити НЕ МОЖНА)
- ❌ «Викликаю refresh() після кожного persist(), щоб перевірити, що дані точно збереглися» — Антипатерн, що подвоює кількість запитів до БД без будь-якої користі.
- ❌ «refresh() можна викликати для Detached або DTO об’єктів» — Викине
IllegalArgumentException, працює тільки дляManaged. - ❌ «refresh() відправляє локальні зміни з пам’яті в БД» — Рівно навпаки! З пам’яті в БД відправляє
flush(), аrefresh()читає з БД і стирає зміни в пам’яті. - ❌ «refresh() автоматично комітить транзакцію» — Ні, він тільки читає рядок поточним з’єднанням.