Урок по треку Spring и Spring Boot 8 мин

Hibernate: N+1, ленивая загрузка, кеш

Состояния сущностей, контекст персистентности, ленивая загрузка, N+1, кеши первого и второго уровня.

Hibernate самый частый источник проблем производительности в Spring-приложениях и самая любимая тема middle-собесов: N+1, LazyInitializationException, кеши, состояния сущностей.

Состояния сущности

СостояниеЧто значит
transientсоздана через new, Hibernate о ней не знает
managed (persistent)связана с открытым контекстом персистентности, изменения отслеживаются
detachedбыла managed, но контекст закрыт или она отсоединена
removedпомечена на удаление, DELETE уйдёт при flush

Контекст персистентности (EntityManager, Session) это кеш первого уровня и единица работы: внутри одной транзакции два find одного id вернут один и тот же объект, а изменения managed-сущностей сами превратятся в UPDATE при flush (dirty checking: Hibernate сравнивает поля со снимком, сделанным при загрузке).

Flush происходит при коммите, перед JPQL-запросом, который может затронуть изменённые сущности (режим AUTO), и при явном flush().

Ленивая и жадная загрузка

По умолчанию в JPA: @ManyToOne и @OneToOne EAGER, @OneToMany и @ManyToMany LAZY. Рекомендация: делать LAZY все связи и загружать нужное явно.

Ленивая связь это прокси или обёртка коллекции. Первое обращение выполняет запрос. Если контекст уже закрыт, получаем LazyInitializationException.

N+1

List<Order> orders = orderRepo.findAll();          // 1 запрос
for (Order o : orders) {
    o.getItems().size();                           // + N запросов
}

Тот же эффект дают EAGER @ManyToOne при загрузке списка через JPQL и сериализация сущностей в JSON. Как лечить:

  • join fetch в JPQL: select o from Order o join fetch o.items;
  • @EntityGraph(attributePaths = {"items"}) на методе репозитория;
  • @BatchSize(size = 50) или hibernate.default_batch_fetch_size: ленивые коллекции догружаются пачками через IN (...), N+1 превращается в 1 + N/50;
  • DTO-проекция, когда нужны только поля.

Ограничения join fetch: нельзя одновременно fetch-джойнить две коллекции типа List (MultipleBagFetchException, декартово произведение), и fetch коллекций ломает пагинацию (Hibernate делает её в памяти). Находят N+1 через логи SQL, hibernate.generate_statistics, тесты с подсчётом запросов.

Open Session in View

В Spring Boot по умолчанию spring.jpa.open-in-view=true: EntityManager открыт до конца HTTP-запроса, и ленивые связи грузятся даже в контроллере и при сериализации. Ошибка LazyInitializationException исчезает, но появляются скрытые N+1 и соединение с БД держится весь запрос. Boot предупреждает об этом при старте. В серьёзных проектах OSIV выключают и загружают нужное явно в сервисе.

Кеш второго уровня

  • L1 (кеш первого уровня): внутри контекста персистентности, всегда включён, живёт одну транзакцию.
  • L2: общий для всех сессий (SessionFactory), выключен по умолчанию, требует провайдера (Ehcache, Infinispan, Caffeine через JCache) и @Cache на сущностях. Подходит для редко меняющихся справочников.
  • Кеш запросов: хранит id результатов запроса, инвалидируется при любом изменении таблицы.

Риск L2: в кластере нужно согласовать инвалидацию, а изменения в обход Hibernate (другой сервис, SQL руками) кеш не увидит.

Связи и каскады

  • mappedBy задаёт владельца двунаправленной связи: внешний ключ пишет сторона без mappedBy (обычно @ManyToOne).
  • cascade = ALL и orphanRemoval = true уместны для агрегата (заказ и его позиции), но не для связей с независимыми сущностями.
  • equals и hashCode сущности не строят на сгенерированном id, который до persist равен null, и не включают ленивые связи.

Частые ошибки

  • EAGER ради «чтобы работало», а потом сотни запросов.
  • LazyInitializationException лечат включением OSIV или FetchType.EAGER.
  • Две List-коллекции в одном join fetch.
  • Lombok @Data на сущности: toString и hashCode трогают ленивые связи.

Что спросят на собесе

  • Что такое N+1, как его найти и исправить?
  • Какие состояния бывают у сущности? Что такое dirty checking?
  • Откуда берётся LazyInitializationException и как её правильно избегать?
  • Что такое Open Session in View и почему его выключают?
  • Чем кеш первого уровня отличается от второго?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.