💾 Розділ 16 · Питання #15

Що робить метод 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()

  1. Працює лише для сутностей зі статусом Managed: Якщо передати об’єкт у стані Detached або Transient, Hibernate негайно викине виняток: java.lang.IllegalArgumentException: Entity not managed.
  2. Затирає незбережені дані: Усі зміни полів об’єкта, зроблені в Java-коді до виклику refresh(), безповоротно втрачаються (якщо попередньо не було виконано flush()).
  3. Оновлює знімок 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

  1. Робота тригерів бази даних: Якщо в СУБД налаштовані тригери (BEFORE INSERT / BEFORE UPDATE), які обчислюють колонки (наприклад, генерують промокод, статус або рахують підсумкову суму), Hibernate не знає про ці зміни.
    entityManager.persist(order);
    entityManager.flush(); // Фізично відправляє INSERT, тригер БД спрацьовує
    entityManager.refresh(order); // Завантажує обчислені тригером поля
    
  2. Отримання колонок зі значеннями за замовчуванням (DEFAULT): Коли колонка ініціалізується базою даних (created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP), виклик refresh() підтягує згенеровані СУБД таймстемпи.
  3. Після масових оновлень (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:

  1. Перевіряється статус в entityEntries. Якщо сутність не Status.MANAGED, викидається IllegalArgumentException.
  2. Якщо в ActionQueue були накопичені незафіксовані дії над цією сутністю, вони можуть конфліктувати зі скиданням.
  3. Hibernate формує SQL SELECT з усіма змаппленими колонками та виконує його через JDBC.
  4. Значення колонок витягуються з ResultSet і записуються в поля цільового Java-об’єкта (через прямий доступ до полів за Field.set() або сетери).
  5. Критичний крок: Масив loadedState всередині EntityEntry повністю перезаписується новими значеннями. Це гарантує, що наступний запуск Dirty Checking порахує сутність абсолютно «чистою» (not dirty).
  6. Поведінка колекцій (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?

Відповідь:

  1. SQL UPDATE відправлено не буде.
  2. Метод refresh() виконує негайний SQL SELECT, читає старий email з БД і перезаписує поле об’єкта в пам’яті JVM.
  3. Одночасно з цим Hibernate оновлює знімок loadedState у контексті PersistenceContext.
  4. Наприкінці транзакції Dirty Checking порівнює поточне поле об’єкта (яке містить старий email) зі знімком (який також містить старий email). Відмінностей немає — жодних UPDATE не планується.

2. Що станеться, якщо викликати entityManager.refresh(user), але рядок з цим ID було видалено в базі даних паралельною транзакцією?

Відповідь:

  1. Метод refresh() виконає SELECT * FROM users WHERE id = ?.
  2. База даних поверне порожній ResultSet (0 рядків).
  3. Специфікація JPA вимагає в такому випадку негайно викинути виняток: jakarta.persistence.EntityNotFoundException: Entity no longer exists in the database.
  4. Транзакція буде переведена в режим Rollback-Only.

3. Ми виконали запит у Spring Data JPA: @Modifying @Query("UPDATE User u SET u.active = false"). Чому без виклику refresh() або спеціального параметра анотації ми побачимо старі дані?

Відповідь:

  1. Анотація @Modifying виконує прямий SQL/JPQL UPDATE на рівні СУБД, повністю оминаючи PersistenceContext (кеш L1).
  2. Об’єкти User, що вже знаходяться в пам’яті поточної сесії, нічого не знають про цю зміну і продовжують зберігати active = true.
  3. Щоб виправити розсинхронізацію, необхідно або викликати entityManager.refresh(user) для кожної сутності, або використовувати параметр анотації @Modifying(clearAutomatically = true). Параметр clearAutomatically = true очистить весь кеш L1, змусивши наступний find() прочитати свіжі дані з бази.

4. Чи може виклик refresh() призвести до NonUniqueObjectException, якщо в сесії є пов’язані ліниві сутності?

Відповідь:

  1. Ні, для самої оновлюваної сутності це виключено завдяки Identity Map: оновлюється саме існуючий екземпляр у пам’яті за посиланням.
  2. Проте, якщо під час каскадного refresh(order) перезавантажується колекція, що містить дочірні елементи, які раніше були від’єднані (Detached) і заново створені в паралельному коді без участі сесії, спроба повторної прив’язки дублюючого екземпляра може призвести до конфлікту ідентичності в PersistenceContext.

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь (Elevator Pitch)

«Метод entityManager.refresh() примусово перезавантажує стан Managed сутності з бази даних, виконуючи SQL SELECT за первинним ключем. Він затирає будь-які незбережені локальні зміни в полях об’єкта та оновлює знімок 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() автоматично комітить транзакцію» — Ні, він тільки читає рядок поточним з’єднанням.

Пов’язані теми