- Какие есть кэши в Hibernate и какие работают по умолчанию?
- Чем отличается Lazy от Eager в Hibernate?
- Что такое "проблема N+1 запроса" при использовании Hibernate? Когда возникает? Как решить? Как обнаружить?
- Как описать составной ключ при использовании Hibernate?
- Как можно отобразить наследование на БД с помощью JPA (Hibernate)?
3 уровня кеширования:
- Кеш первого уровня (First-level cache). По умолчанию включен.
- Кеш второго уровня (Second-level cache). По умолчанию отключен.
- Кеш запросов (Query cache). По умолчанию отключен.
Подробнее:
- Статья на Хабре
- Документация и на Хабре есть перевод
- Статья на Baeldung
- Статьи с практическими примерами о First Level Cache и Second Level Cache
Углубиться в Hibernate нам всегда поможет Vlad Mihalcea:
- Конечно же в его книге High-Performance Java Persistence: Часть 2, глава 16 - Caching
- How does Hibernate Query Cache work
- How does Hibernate Collection Cache work
- How does Hibernate store second-level cache entries
- How does Hibernate NONSTRICT_READ_WRITE CacheConcurrencyStrategy work
- How does Hibernate TRANSACTIONAL CacheConcurrencyStrategy work
- How does Hibernate READ_ONLY CacheConcurrencyStrategy work
- How does Hibernate READ_WRITE CacheConcurrencyStrategy work
- Eager Loading - стратегия загрузки, при которой подгрузка связанных сущностей происходит сразу. Для применения необходимо в аннотацию отношения (
@OneToOne
,@ManyToOne
,@OneToMany
,@ManyToMany
) передатьfetch = FetchType.EAGER
. Используется по умолчанию для отношений@OneToOne
и@ManyToOne
. - Lazy Loading - стратегия загрузки, при которой подгрузка связанных сущностей откладывается как можно дольше. Чтобы задать такое поведение, нужно в аннотацию отношения (
@OneToOne
,@ManyToOne
,@OneToMany
,@ManyToMany
) передатьfetch = FetchType.LAZY
. Используется по умолчанию для отношений@OneToMany
,@ManyToMany
. До момента загрузки используется proxy-объект, вместо реального. Если обратиться к такому LAZY-полю после закрытия сессии Hibernate, то получим LazyInitializationException.
Вопрос также связан с проблемой "N+1" и может плавно перетечь в её обсуждение.
Почитать подробнее и с примерами можно в блоге Vlad Mihalcea: раз, два и про LazyInitializationException три.
Что такое "проблема N+1 запроса" при использовании Hibernate? Когда возникает? Как решить? Как обнаружить?
Проблема N+1 может возникнуть не только при использовании Hibernate, но и других библиотек и фреймворков для доступа к данным.
В общем случае говорят о проблеме N+1 запроса, когда фреймворк выполняет N дополнительных запросов выборки данных, когда можно было обойтись всего одним. Соответственно от размера N зависит влияние проблемы на время ответа нашего приложения. Эту ситуацию нельзя обнаружить с помощью slow query log
, ибо сами по себе запросы могут выполняться быстро, но их количество окажется большим или даже огромным.
На такое можно нарваться даже при использовании plain sql (jdbc, JOOQ), когда у нас одна сущность (и соответственно таблица) связана с другой. И вот мы подгрузили одним запросом просто список из первых, а потом пошли и в цикле для каждой подгрузили связанную по одному запросу. "Да как вы это допустили!?". Да просто по запарке кто-то в цикле начал вызывать метод, у которого в глубине где-то делается запрос и привет. Как исправить? Использовать JOIN
со связанной таблицей при чтении списка. Тогда понадобиться лишь один запрос.
Теперь к Hibernate. Если на странице документации поискать "N+1", то можно обнаружить несколько упоминаний данной проблемы. Тут опишу самые явные и распространённые.
Например, возьмём стратегию выборки FetchType.EAGER
. Она склонна к порождению N+1. А в отношении @ManyToOne
по умолчанию используется именно она. Забыли в своём JPQL запросе заиспользовать JOIN FETCH
и привет. А если нам и не нужны были связанные сущности, то тогда стоит задать стратегию FetchType.LAZY
.
Если уж упомянули FetchType.LAZY
, то сразу стоит сказать, что одно её наличие не гарантирует отсутствие проблемы N+1. При выборке списка сущностей, связанные автоматически не подгрузились. А мы потом пошли в цикле по загруженному списку и стали обращаться к полям связанной сущности - и снова здравствуйте. Всё тот же JOIN FETCH
нас спасёт и в этой ситуации.
Но JOIN FETCH
во многих случаях нас может привести к декартовому произведению, и тогда будет совсем bonjour. Для отношения @OneToMany
это можно решить с помощью FetchMode.SUBSELECT
- будет 2 запроса, но во втором запросе на получение списка связанных сущностей в условии выборки будет подзапрос на получение идентификаторов родительских сущностей. Т.е. запрос практически повторяется и он может быть тяжеловесным.
Есть вариант лучше - вычитывать связанные сущности пачками. Мы можем добавить аннотацию @BatchSize
и указать размер подгружаемой пачки записей в одном запросе.
Ещё варианты:
Чтобы обнаружить проблему N+1, нужно писать тесты с использованием библиотеки db-util от Vlad Mihalcea. Подробнее можно прочитать у него же в блоге.
А вот JOOQ умеет обнаруживать N+1 автоматически, послушать об этом можно в 17-м эпизоде (01:16:36) подкаста Паша+Слава. Подробнее в документации JOOQ.
Углубиться в проблему можно:
- В блоге Vlad Mihalcea: раз и два
- На хабре упоминалась в статье Hibernate — о чем молчат туториалы
- На DOU про стратегии загрузки коллекций в JPA и Hibernate
- В видео формате в докладе Николая Алименкова на JPoint "Сделаем Hibernate снова быстрым"
На всякий случай: составной ключ
- первичный ключ, состоящий из двух и более атрибутов. Вообще про ключи есть большая статья на Хабре с ценными комментариями.
Чтобы описать составной ключ при использовании Hibernate, нам необходимо создать под этот ключ отдельный класс с необходимыми полями и добавить ему аннотацию @Embeddable
. Кроме того, он должен быть Serializable
и иметь реализацию equals
и hashcode
.
В самой же сущности, для которой мы описываем составной ключ, добавляем поле только что созданного класса ключа и вешаем на него аннотацию @EmbeddedId
.
Посмотреть примеры и углубиться в тему можно в статье Vlad Mihalcea или в документации Hibernate.
Есть 4 способа отобразить наследование на БД с помощью JPA (Hibernate):
- MappedSuperclass - поля родителя содержатся в каждой таблице для каждого дочернего класса. Базовый класс отдельной таблицы не имеет. На базовый класс навешиваем @MappedSuperClass, а вот на дочерние
@Entity
. Если в таблице потомка поле родителя называется не так, как указано в родительском классе, то его нужно смаппить с помощью аннотации @AttributeOverride в классе этого потомка. Родитель не может участвовать в ассоциации. При полиморфных запросах у нас будут отдельные запросы для каждой таблицы. - Single table - вся иерархия классов в одной таблице. Чтобы различать классы, необходимо добавить колонку-дискриминатор. В данной стратегии на родительский
@Entity
-класс навешивается@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
и@DiscriminatorColumn(name = "YOUR_DISCRIMINATOR_COLUMN_NAME")
(по умолчанию имя колонкиDTYPE
и типVARCHAR
). В каждом подклассе указываем@DiscriminatorValue("ThisChildName")
со значением, которое будет храниться в колонке-дискриминаторе для данного класса. Если нет возможности добавить колонку, то можно использовать аннотацию @DiscriminatorFormula, в которой указать выражениеCASE...WHEN
- это не по JPA, фишка Hibernate. Денормализация. Простые запросы к одной таблице. Возможное нарушение целостности - столбцы подклассов могут содержатьNULL
. - Joined table - отдельные таблицы для всех классов иерархии, включая родителя. В каждой таблице только свои поля, а в дочерних добавляется внешний (он же первичный) ключ для связи с родительской таблицей. В
@Entity
-класс родителя добавляем@Inheritance(strategy = InheritanceType.JOINED)
. Для полиморфных запросов используютсяJOIN
, а также выражениеCASE...WHEN
, вычисляющее значение поля_clazz
, которое заполняется литералами (0 (родитель), 1, 2 и т.д.) и помогает Hibernate определить какого класса будет экземпляр. - Table per class - также как и в
MappedSuperclass
, имеем отдельные таблицы для каждого подкласса. Базовый класс отдельной таблицы не имеет. По спецификации JPA 2.2 (раздел 2.12) данная стратегия является опциональной, но в Hibernate реализована, поэтому продолжим. В данном случае на базовый класс мы навешиваем@Entity
и@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
. Поле первичного ключа (@Id
) обязательно для родительского класса. Также аннотация@AttributeOverride
в этой стратегии не работает - называйте родительские поля в таблицах сразу единообразно. Полиморфный запрос будет использоватьUNION
для объединения таблиц. Чтобы различить при создании экземпляров подклассы, Hibernate добавляет поле_clazz
в запросы, содержащие литералы (1, 2 и т.д.). А одинаковый набор столбцов для объединения добирается какNULL AS some_field
. Родитель может участвовать в ассоциации с другими сущностями.
Почитать ещё по теме с примерами можно:
- На Хабре: раз, два
- В блоге Vlad Mahalcea: Влад демонстрирует лучшую стратегию и рассказывает про MappedSuperclass
Кроме того, можно заглянуть в JavaDoc с аннотациями:
Или почитать спецификацию JPA 2.2. Ещё есть книжка с толкованием данного pdf.