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 и почему его выключают?
- Чем кеш первого уровня отличается от второго?