Питання 9 · Розділ 16

Що таке кеш першого рівня в Hibernate

Кеш першого рівня (L1 cache) — вбудований кеш на рівні EntityManager/Session. Він є фундаментальною частиною Hibernate і забезпечує ідентичність сутностей, dirty checking та опт...

Мовні версії: English Russian Ukrainian

Огляд

Кеш першого рівня (L1 cache) — вбудований кеш на рівні EntityManager/Session. Він є фундаментальною частиною Hibernate і забезпечує ідентичність сутностей, dirty checking та оптимізацію запитів.

Навіщо: L1 cache вирішує три проблеми: (1) Identity guarantee — один ID = один об’єкт в пам’яті; (2) Dirty checking — потрібно зберігати snapshot для порівняння; (3) Оптимізація — повторний find() не робить SELECT.


🟢 Junior Level

Що таке L1 cache

Кеш першого рівня — це кеш всередині EntityManager (сесії). Всі завантажені сутності зберігаються в ньому автоматично.

// Перший запит — SELECT з БД
User user1 = entityManager.find(User.class, 1L);
// SQL: SELECT * FROM users WHERE id = 1

// Другий запит — з кешу, БЕЗ SELECT!
User user2 = entityManager.find(User.class, 1L);
// SQL: (немає запиту!)

assert user1 == user2;  // true — один і той самий об'єкт в пам'яті

Основні властивості

  • ✅ Увімкнений за замовчуванням, не можна вимкнути
  • 🔄 Живе в межах однієї сесії/транзакції
  • 🗑️ Автоматично очищується при закритті EntityManager
  • 🔒 Забезпечує identity guarantee (один ID = один об’єкт). Якщо в одній транзакції двічі завантажити User(1), Hibernate поверне ОДИН І ТОЙ САМ об’єкт в пам’яті (user1 == user2). Це потрібно щоб dirty checking працював коректно — інакше зміни одного об’єкта могли б конфліктувати з іншим.

Коли спрацьовує

// find() — перевіряє кеш перед запитом до БД
User user1 = entityManager.find(User.class, 1L);  // SELECT
User user2 = entityManager.find(User.class, 1L);  // з кешу

// persist() — додає в кеш
User newUser = new User();
entityManager.persist(newUser);  // в кеші

// merge() — додає оновлену копію в кеш
User managed = entityManager.merge(detachedUser);  // в кеші

🟡 Middle Level

Коли L1 cache шкідливий

  1. Завантаження 10k+ сутностей в одній транзакції — OutOfMemoryError
  2. Streaming великих даних — використовуйте ScrollableResults
  3. Пакетні операції — використовуйте StatelessSession

Dirty checking через L1 cache

// Hibernate зберігає snapshot при завантаженні
User user = entityManager.find(User.class, 1L);  // snapshot створено

// Змінюємо поле
user.setName("New Name");

// При commit/flush — Hibernate порівнює snapshot з поточним станом
// Якщо різні — автоматично робить UPDATE
// Не потрібен явний entityManager.merge()!

Управління кешем

// Очистити весь кеш
entityManager.clear();
// Всі сутності стають detached

// Видалити конкретну сутність з кешу
entityManager.detach(user);
// user стає detached, решта залишаються

// Перезавантажити з БД (оновити кеш)
entityManager.refresh(user);
// SELECT з БД, оновлення snapshot

// Перевірити наявність в кеші
boolean isManaged = entityManager.contains(user);

Проблема великого L1 cache

// ❌ Завантажили 100k entities → OutOfMemoryError!
for (Long id : allIds) {
    entityManager.find(User.class, id);  // всі в кеші
}

// ✅ Рішення: періодичне очищення
for (int i = 0; i < allIds.size(); i++) {
    entityManager.find(User.class, allIds.get(i));

    if (i % 100 == 0) {
        entityManager.flush();   // скинути в БД
        entityManager.clear();   // очистити кеш
    }
}

🔴 Senior Level

L1 cache vs ручний Map

На відміну від ручного Map, L1 cache: (1) автоматично заповнюється при будь-якому запиті; (2) забезпечує identity guarantee; (3) зберігає snapshot для dirty checking; (4) автоматично очищується при закритті EntityManager.

Внутрішня реалізація

StatefulPersistenceContext {
    entitiesByKey: Map<EntityKey, Object>
    entityEntries: Map<Object, EntityEntry>
}

EntityEntry містить:
- loadedState — snapshot при завантаженні
- status — MANAGED, DELETED, READ_ONLY
- id — identifier
- version — для optimistic locking

Read-only hint для оптимізації

List<User> users = entityManager.createQuery("FROM User", User.class)
    .setHint("org.hibernate.readOnly", true)
    .getResultList();

// Переваги:
// 1. Не створюється snapshot (економія пам'яті)
// 2. Dirty checking не виконується
// 3. Менше overhead на flush

Best Practices

✅ L1 cache працює за замовчуванням
✅ entityManager.clear() для пакетних операцій
✅ read-only hint для запитів без змін
✅ flush + clear кожні 50-100 entities в batch

❌ Завантаження великої кількості entities без clear
❌ Використання L1 cache як кеш між запитами

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • L1 cache — вбудований кеш EntityManager, увімкнений за замовчуванням, не можна вимкнути
  • Вирішує 3 проблеми: identity guarantee, dirty checking (snapshot), оптимізація запитів
  • find() перевіряє кеш перед запитом до БД — повторний find() без SQL
  • Живе в межах однієї транзакції, очищується при закритті EntityManager
  • При завантаженні 10k+ сутностей — OutOfMemoryError, потрібен clear()
  • Read-only hint економить пам’ять (не створює snapshot)

Часті уточнюючі запитання:

  • Чи можна вимкнути L1 cache? Ні, це фундаментальна частина Hibernate
  • Як уникнути OOM при batch? flush() + clear() кожні 50-100 entities
  • L1 cache vs ручний Map? L1 cache автоматично заповнюється, забезпечує identity guarantee і зберігає snapshot

Червоні прапорці (НЕ говорити):

  • «Використовую L1 cache як кеш між запитами» — він живе тільки в рамках однієї сесії
  • «Вимикаю L1 cache для продуктивності» — не можна вимкнути
  • «Завантажую 100k entities без clear» — OutOfMemoryError

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

  • [[10. Що таке кеш другого рівня і коли його використовувати]]
  • [[11. Як налаштувати кеш другого рівня]]
  • [[12. Що таке dirty checking в Hibernate]]
  • [[13. Як працює механізм flush в Hibernate]]