Вопрос 12 · Раздел 16

Что такое dirty checking в Hibernate

Dirty checking — механизм автоматического отслеживания изменений сущностей в persistence context и их сохранения в базу данных. Это одна из ключевых возможностей Hibernate, кото...

Версии по языкам: English Russian Ukrainian

Обзор

Dirty checking — механизм автоматического отслеживания изменений сущностей в persistence context и их сохранения в базу данных. Это одна из ключевых возможностей Hibernate, которая избавляет разработчика от ручного написания UPDATE-запросов.


🟢 Junior Level

Что такое dirty checking

Dirty checking — Hibernate автоматически отслеживает изменения у сущностей в persistence context и при сохранении автоматически генерирует UPDATE-запрос.

@Transactional
public void updateUser(Long id, String name) {
    // 1. Загрузка из БД — сущность становится Persistent
    User user = entityManager.find(User.class, id);

    // 2. Изменение поля — Hibernate "замечает" изменение
    user.setName(name);

    // 3. НЕ НУЖЕН em.merge() или em.save()!
    // При commit — Hibernate автоматически сделает UPDATE
}

Как это работает — просто

1. Загрузка: Hibernate загружает entity и сохраняет его "снимок" (snapshot)
2. Изменение: вы меняете поле в объекте
3. Flush: Hibernate сравнивает snapshot с текущим состоянием
4. Если разные → генерирует UPDATE SQL
5. Если одинаковые → ничего не делает

Пример

@Transactional
public void updateEmail(Long userId, String newEmail) {
    User user = entityManager.find(User.class, userId);  // snapshot
    user.setEmail(newEmail);                              // изменение
    // При выходе из метода → commit → flush → UPDATE
}

// Эквивалентный SQL:
// UPDATE users SET email = ? WHERE id = ?

🟡 Middle Level

Детальный механизм dirty checking

Шаг 1: Загрузка entity
- entityManager.find(User.class, 1L)
- Hibernate создаёт EntityEntry с snapshot
- snapshot = копия всех полей на момент загрузки

Шаг 2: Изменение
- user.setName("New Name")
- Hibernate НЕ делает ничего сразу

Шаг 3: Flush (при commit или явно)
- Hibernate обходит все entities в persistence context
- Для каждого: сравнивает snapshot с текущим состоянием
- Если есть изменения → schedule UPDATE
- Выполняет все UPDATE в БД
- Обновляет snapshot

Когда происходит flush

// 1. При commit транзакции (автоматически)
@Transactional
public void update() {
    user.setName("New");
}  // commit → flush → UPDATE

// 2. При entityManager.flush() (явно)
entityManager.flush();  // UPDATE выполнен

// 3. Перед выполнением запроса (чтобы вернуть актуальные данные)
user.setName("New");
List<User> users = entityManager.createQuery("FROM User", User.class)
    .getResultList();  // перед этим — flush!

Оптимизация — read-only сущности

// Если сущность не будет изменяться — пометить как read-only
@Query("SELECT u FROM User u WHERE u.id = :id")
@Lock(LockModeType.OPTIMISTIC)
User findReadOnly(@Param("id") Long id);

// Или через hint
User user = entityManager.createQuery("FROM User u WHERE u.id = :id", User.class)
    .setParameter("id", id)
    .setHint("org.hibernate.readOnly", true)
    .getSingleResult();

// Преимущества:
// 1. Не создаётся snapshot (экономия памяти)
// 2. Dirty checking не выполняется
// 3. Меньше overhead на flush

Типичные ошибки

// ❌ Лишний UPDATE
user.setName(user.getName());  // значение не изменилось
// Hibernate выполнит dirty check, обнаружит что значение не изменилось, и UPDATE не выполнит. Однако сам check имеет overhead для каждого поля — при 10k сущностях это миллионы сравнений.

// ❌ Dirty checking для больших контекстов
@Transactional
public void processAll() {
    List<User> users = userRepository.findAll();  // 10k users
    for (User user : users) {
        // dirty checking для 10k entities — медленно!
        processUser(user);
    }
}

// ✅ Решение: periodic clear
@Transactional
public void processAll() {
    List<User> users = userRepository.findAll();
    for (int i = 0; i < users.size(); i++) {
        processUser(users.get(i));
        if (i % 100 == 0) {
            entityManager.flush();
            entityManager.clear();  // сбросить dirty checking
        }
    }
}

🔴 Senior Level

Внутренняя реализация

PersistenceContext:

StatefulPersistenceContext {
    entityEntries: Map<Object, EntityEntry>
}

EntityEntry {
    loadedState: Object[]    // snapshot при загрузке
    state: Object[]          // текущее состояние
    status: Status          // MANAGED, DELETED, READ_ONLY
    id: Serializable
    version: Object
}

Алгоритм flush:
1. Для каждого entity в entityEntries:
   if entity.status == MANAGED:
     if !Arrays.equals(entry.loadedState, entry.state):
       scheduleUpdate(entity)
2. Выполнить все scheduled UPDATE
3. Обновить loadedState = state

Performance характеристики

Dirty checking overhead:
- O(N) где N = число entities в persistence context
- Для каждого entity: array comparison всех полей
- Для 10k+ entities — может быть заметно медленно

Оптимизации:
- read-only hint → O(0) (пропускает entity)
- clear() периодически → уменьшает N
- StatelessSession → без dirty checking

@SelectBeforeUpdate

@Entity
@SelectBeforeUpdate(true)
public class User {
    // Перед UPDATE Hibernate сделает SELECT
    // Чтобы проверить, действительно ли данные изменились
    // Полезно когда сущность часто "обновляется" без реальных изменений
}

Trade-off: один лишний SELECT ценой избегания ненужного UPDATE. Выгодно, когда сущность часто «сохраняют» без реальных изменений и UPDATE дорогой (триггеры, аудит).

Batch updates

@Transactional
public void batchUpdate(List<User> users) {
    for (int i = 0; i < users.size(); i++) {
        User managed = entityManager.merge(users.get(i));
        managed.setStatus("processed");

        if (i % 50 == 0) {
            entityManager.flush();    // выполнить UPDATE
            entityManager.clear();    // очистить context
        }
    }
}

Best Practices

✅ Dirty checking для простых обновлений
✅ Read-only hint для запросов без изменений
✅ entityManager.clear() для больших операций
✅ Понимание когда происходит flush
✅ Периодический flush + clear в batch операциях

❌ Dirty checking для больших persistence context
❌ Без read-only hint когда не нужно обновление
❌ Игнорирование performance impact
❌ Ручной UPDATE когда dirty checking достаточно

🎯 Шпаргалка для интервью

Обязательно знать:

  • Dirty checking — Hibernate автоматически отслеживает изменения managed-сущностей
  • При загрузке сохраняется snapshot, при flush сравнивается с текущим состоянием
  • Не нужен explicit merge() или save() для обновлений managed-сущностей
  • O(N) overhead где N = число entities в persistence context
  • Flush происходит при: commit, entityManager.flush(), перед запросом
  • Read-only hint экономит память — не создаётся snapshot, dirty checking пропускается

Частые уточняющие вопросы:

  • Когда происходит UPDATE? При flush: snapshot != current state → генерируется UPDATE SQL
  • Почему dirty checking медленный для 10k+ entities? O(N) array comparison для каждого поля каждой сущности
  • Что такое @SelectBeforeUpdate? SELECT перед UPDATE чтобы проверить изменились ли данные — trade-off: SELECT vs ненужный UPDATE
  • Как оптимизировать? Read-only hint, clear() периодически, StatelessSession для массовых операций

Красные флаги (НЕ говорить):

  • «Всегда вызываю merge() после изменения» — для managed не нужен, лишний SELECT
  • «Dirty checking для 100k entities без clear» — O(N) overhead, будет медленно
  • «Без read-only hint для отчётов» — лишний snapshot и dirty checking overhead
  • «Не понимаю когда происходит flush» — критично для транзакций

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

  • [[7. Опишите жизненный цикл Entity в Hibernate]]
  • [[13. Как работает механизм flush в Hibernate]]
  • [[9. Что такое кэш первого уровня в Hibernate]]
  • [[14. В чём разница между persist() и merge()]]