💾 Раздел 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() автоматически коммитит транзакцию» — Нет, он только читает строку текущим соединением.

Связанные темы