Что такое dirty checking в Hibernate
Dirty checking — механизм автоматического отслеживания изменений сущностей в persistence context и их сохранения в базу данных. Это одна из ключевых возможностей Hibernate, кото...
Обзор
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()]]