В предыдущей статьея рассматривал детали различных типов и стратегий загрузки коллекций в JPA. В данной статье будут рассмотрены режимы загрузки коллекций в Hibernate.
Для тех, кто не читал, повторюсь: отношениям один-ко-многим или многие-ко-многим между таблицами реляционной базы данных в объектном виде соответствуют свойства сущности типа List
или Set
, размеченные аннотациями @OneToMany
или @ManyToMany
. При работе с сущностями, которые содержат коллекции других сущностей, возникает проблема известная как «N+1 selects». Первый запрос выберет только корневые сущности, а каждая связанная коллекция будет загружена отдельным запросом. Таким образом ORM выполняет N+1 SQL запросов, где N — количество корневых сущностей в результирующей выборке запроса.
Итак, самая популярная реализация JPA, Hibernate предоставляет множество способов управления загрузкой отношений один-ко-многим и многие-ко-многим.
FetchMode
в Hibernate говорит как мы хотим, чтоб связанные сущности или коллекции были загружены:
— SELECT
— используя по дополнительному SQL запросу на коллекцию,
— JOIN
— в одном запросе с корневой сущностью, используя SQL оператор JOIN,
— SUBSELECT
— в дополнительном запросе, используя SUBSELECT
.
Мы также можем влиять на стратегию загрузки связанных коллекций при помощи аннотации @BatchSize
(или атрибут batch-size
в XML), которая устанавливает количество коллекций, которые будут загружаться в одном запросе.
Пример
Продолжим рассматривать пример из предыдущей части статьи, в котором сущность Book владеет отношениями многие-ко-многим с сущностями Author
и Category
. Дополним этот пример, добавив аннотации из пакета org.hibernate.annotations
, которые не являются частью JPA. Пример целиком доступен на Github.
@Entity public class Book extends AbstractBook { @ManyToMany(fetch = FetchType.EAGER) private List<Author> authors = new ArrayList<>(); @ManyToMany private List<Category> categories = new ArrayList<>(); /*...*/ } @Entity public class BookFetchModeSelect extends AbstractBook { @ManyToMany(fetch = FetchType.EAGER) @Fetch(FetchMode.SELECT) private List<Author> authors = new ArrayList<>(); @ManyToMany @Fetch(FetchMode.SELECT) private List<Category> categories = new ArrayList<>(); /*...*/ } @Entity public class BookFetchModeJoin extends AbstractBook { @ManyToMany(fetch = FetchType.EAGER) @Fetch(FetchMode.JOIN) private List<Author> authors = new ArrayList<>(); @ManyToMany @Fetch(FetchMode.JOIN) private List<Category> categories = new ArrayList<>(); /*...*/ } @Entity public class BookFetchModeSubselect extends AbstractBook { @ManyToMany(fetch = FetchType.EAGER) @Fetch(FetchMode.SUBSELECT) private List<Author> authors = new ArrayList<>(); @ManyToMany @Fetch(FetchMode.SUBSELECT) private List<Category> categories = new ArrayList<>(); /*...*/ } @Entity public class BookBatchSize extends AbstractBook { @ManyToMany(fetch = FetchType.EAGER) // Явное указание FetchMode.SELECT необходимо // так как в Criteria API EAGER ассоциации по умолчанию загружаются с @Fetch(FetchMode.SELECT) @BatchSize(size = 2) private List<Author> authors = new ArrayList<>(); @ManyToMany @Fetch(FetchMode.SELECT) @BatchSize(size = 2) private List<Category> categories = new ArrayList<>(); /*...*/ }
Для более наглядной демонстрации отличий режимов загрузки необходимо добавить больше тестовых данных:
Session session = sessionFactory.getCurrentSession(); Category softwareDevelopment = new Category(); softwareDevelopment.setName("Software development"); session.persist(softwareDevelopment); Category systemDesign = new Category(); systemDesign.setName("System design"); session.persist(systemDesign); Author martinFowler = new Author(); martinFowler.setFullName("Martin Fowler"); session.persist(martinFowler); AbstractBook poeaa = bookSupplier.get(); poeaa.setIsbn("007-6092019909"); poeaa.setTitle("Patterns of Enterprise Application Architecture"); poeaa.setPublicationDate(Date.from(Instant.parse("2002-11-15T00:00:00.00Z"))); poeaa.setAuthors(asList(martinFowler)); poeaa.setCategories(asList(softwareDevelopment, systemDesign)); session.persist(poeaa); Author gregorHohpe = new Author(); gregorHohpe.setFullName("Gregor Hohpe"); session.persist(gregorHohpe); Author bobbyWoolf = new Author(); bobbyWoolf.setFullName("Bobby Woolf"); session.persist(bobbyWoolf); AbstractBook eip = bookSupplier.get(); eip.setIsbn("978-0321200686"); eip.setTitle("Enterprise Integration Patterns"); eip.setPublicationDate(Date.from(Instant.parse("2003-10-20T00:00:00.00Z"))); eip.setAuthors(asList(gregorHohpe, bobbyWoolf)); eip.setCategories(asList(softwareDevelopment, systemDesign)); session.persist(eip); Category objectOrientedSoftwareDesign = new Category(); objectOrientedSoftwareDesign.setName("Object-Oriented Software Design"); session.persist(objectOrientedSoftwareDesign); Author ericEvans = new Author(); ericEvans.setFullName("Eric Evans"); session.persist(ericEvans); AbstractBook ddd = bookSupplier.get(); ddd.setIsbn("860-1404361814"); ddd.setTitle("Domain-Driven Design: Tackling Complexity in the Heart of Software"); ddd.setPublicationDate(Date.from(Instant.parse("2003-08-01T00:00:00.00Z"))); ddd.setAuthors(asList(ericEvans)); ddd.setCategories(asList(softwareDevelopment, systemDesign, objectOrientedSoftwareDesign)); session.persist(ddd); Category networkingCloudComputing = new Category(); networkingCloudComputing.setName("Networking & Cloud Computing"); session.persist(networkingCloudComputing); Category databasesBigData = new Category(); databasesBigData.setName("Databases & Big Data"); session.persist(databasesBigData); Author pramodSadalage = new Author(); pramodSadalage.setFullName("Pramod J. Sadalage"); session.persist(pramodSadalage); AbstractBook nosql = bookSupplier.get(); nosql.setIsbn("978-0321826626"); nosql.setTitle("NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence"); nosql.setPublicationDate(Date.from(Instant.parse("2012-08-18T00:00:00.00Z"))); nosql.setAuthors(asList(pramodSadalage, martinFowler)); nosql.setCategories(asList(networkingCloudComputing, databasesBigData)); session.persist(nosql);
В тестах используется Hibernate 5.2.0.Final.
HQL запрос и FetchMode по умолчанию
Когда аннотация @Fetch
не добавлена, HQL и Hibernate Criteria запросы ведут себя по-разному. В случае использования HQL запроса, по умолчанию используется FetchMode.SELECT
для связанных коллекций с любым типом загрузки (EAGER
и LAZY
).
List books = getCurrentSession().createQuery("select b from Book b").list(); assertEquals(4, books.size());
Сгенерированный SQL:
select book0_.id as id1_1_, book0_.isbn as isbn2_1_, book0_.publicationDate as publicat3_1_, book0_.title as title4_1_ from Book book0_ select authors0_.Book_id as Book_id1_2_0_, authors0_.authors_id as authors_2_2_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from Book_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.Book_id=? select authors0_.Book_id as Book_id1_2_0_, authors0_.authors_id as authors_2_2_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from Book_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.Book_id=? select authors0_.Book_id as Book_id1_2_0_, authors0_.authors_id as authors_2_2_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from Book_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.Book_id=? select authors0_.Book_id as Book_id1_2_0_, authors0_.authors_id as authors_2_2_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from Book_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.Book_id=?
Hibernate Criteria запрос и FetchMode по умолчанию
Когда аннотация @Fetch
не добавлена, в Hibernate Criteria запросах по умолчанию используется FetchMode.JOIN
для связанных коллекций с типом загрузки EAGER
и tchMode.SELECT
для связанных коллекций с типом загрузки LAZY
.
Коллекции с типом загрузки EAGER
будут загружены в одном SQL запросе с корневой сущностью. Результатом запроса будет декартово произведение (cartesian product). Вместо 4 элементов в результирующей выборке, запрос вернет 6, потому что книги «Enterprise Integration Patterns» и «NoSQL» имеют двух авторов и будут дважды встречаться в результатах запроса.
Стоит отметить, что начиная с версии Hibernate 5.2 метод createCriteria
класса org.hibernate.Session
, который создает экземпляр org.hibernate.Criteria
, считает устаревшим и помечен аннотацией @Deprecated
. Вместо этого в Javadocрекомендуется использовать JPA Criteria.
List books = getCurrentSession().createCriteria(Book.class).list(); assertEquals(6, books.size());
Сгенерированный SQL:
select this_.id as id1_1_1_, this_.isbn as isbn2_1_1_, this_.publicationDate as publicat3_1_1_, this_.title as title4_1_1_, authors2_.Book_id as Book_id1_2_3_, author3_.id as authors_2_2_3_, author3_.id as id1_0_0_, author3_.fullName as fullName2_0_0_ from Book this_ left outer join Book_Author authors2_ on this_.id=authors2_.Book_id left outer join Author author3_ on authors2_.authors_id=author3_.id
HQL и Hibernate Criteria запросы и FetchMode.SELECT
С режимом загрузки FetchMode.SELECT
первый запрос выберет только корневые сущности, а каждая связанная коллекция будет загружена отдельным запросом. Так как для данного примера мы сохранили 4 сущности, то запросов будет 5. Один для загрузки сущностей Book
и по одному для каждой из 4 сущностей Book
для загрузки списка сущностей Author
. Список сущностей Category
имеет тип загрузки по умолчанию (LAZY
для коллекций) и будет загружен отдельным запросом при первом обращении в коде.
List books = getCurrentSession().createQuery("select b from BookFetchModeSelect b").list(); assertEquals(4, books.size());
При режиме загрузки FetchMode.SELECT
HQL и Hibernate Criteria запросы ведут себя одинаково.
List books = getCurrentSession().createCriteria(BookFetchModeSelect.class).list(); assertEquals(4, books.size());
Сгенерированный SQL:
select bookfetchm0_.id as id1_10_, bookfetchm0_.isbn as isbn2_10_, bookfetchm0_.publicationDate as publicat3_10_, bookfetchm0_.title as title4_10_ from BookFetchModeSelect bookfetchm0_ select authors0_.BookFetchModeSelect_id as BookFetc1_11_0_, authors0_.authors_id as authors_2_11_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeSelect_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeSelect_id=? select authors0_.BookFetchModeSelect_id as BookFetc1_11_0_, authors0_.authors_id as authors_2_11_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeSelect_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeSelect_id=? select authors0_.BookFetchModeSelect_id as BookFetc1_11_0_, authors0_.authors_id as authors_2_11_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeSelect_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeSelect_id=? select authors0_.BookFetchModeSelect_id as BookFetc1_11_0_, authors0_.authors_id as authors_2_11_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeSelect_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeSelect_id=?<pre><h2>HQL и Hibernate Criteria запросы и FetchMode.SUBSELECT</h2> При режиме загрузки <code>FetchMode.SUBSELECT</code> будет выполнено 2 SQL запроса. Первый загрузит корневые сущности, а второй - связанные коллекции для всех корневых сущностей из результатов первого SQL запроса, используя подзапрос. <pre>List books = getCurrentSession().createQuery("select b from BookFetchModeSubselect b").list(); assertEquals(4, books.size());
HQL и Hibernate Criteria запросы ведут себя одинаково и при режиме загрузки FetchMode.SUBSELECT
.
List books = getCurrentSession().createCriteria(BookFetchModeSubselect.class).list(); assertEquals(4, books.size());
Сгенерированный SQL:
select bookfetchm0_.id as id1_13_, bookfetchm0_.isbn as isbn2_13_, bookfetchm0_.publicationDate as publicat3_13_, bookfetchm0_.title as title4_13_ from BookFetchModeSubselect bookfetchm0_ select authors0_.BookFetchModeSubselect_id as BookFetc1_14_1_, authors0_.authors_id as authors_2_14_1_, author1_.id as id1_0_0_, author1_.fullName as fullName2_0_0_ from BookFetchModeSubselect_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeSubselect_id in ( select bookfetchm0_.id from BookFetchModeSubselect bookfetchm0_ )
HQL запрос и FetchMode.JOIN
Поведение HQL запросов при режиме загрузке FetchMode.JOIN
, на первый взгляд, немного неожиданное. Вместо того, чтобы загрузить связанные коллекции, помеченные аннотацией @Fetch(FetchMode.JOIN)
, в одном запросе с корневыми сущностями, используя SQL оператор JOIN
, HQL запрос транслируется в несколько SQL запросов по типа FetchMode.SELECT
. Но в отличии от FetchMode.SELECT
, при FetchMode.JOIN
будет игнорироваться указанный тип загрузки (LAZY
и EAGER
) и все коллекции будут загружены сразу, а не при первом обращении в коде (поведение соответствующее типу EAGER
).
Таким образом в следующем примере будет выполнено 8 SQL запросов. Один запрос для загрузки корневых сущностей Book
и для каждой из 4 корневых сущностей из результатов первого SQL запроса по одному запросу для загрузки списка сущностей Author
и по одному запросу для загрузки списка сущностей Category
.
List books = getCurrentSession().createQuery("select b from BookFetchModeJoin b").list(); assertEquals(4, books.size());
Сгенерированный SQL:
select bookfetchm0_.id as id1_7_, bookfetchm0_.isbn as isbn2_7_, bookfetchm0_.publicationDate as publicat3_7_, bookfetchm0_.title as title4_7_ from BookFetchModeJoin bookfetchm0_ select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=? select authors0_.BookFetchModeJoin_id as BookFetc1_8_0_, authors0_.authors_id as authors_2_8_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeJoin_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeJoin_id=? select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=? select authors0_.BookFetchModeJoin_id as BookFetc1_8_0_, authors0_.authors_id as authors_2_8_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeJoin_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeJoin_id=? select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=? select authors0_.BookFetchModeJoin_id as BookFetc1_8_0_, authors0_.authors_id as authors_2_8_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeJoin_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeJoin_id=? select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=? select authors0_.BookFetchModeJoin_id as BookFetc1_8_0_, authors0_.authors_id as authors_2_8_0_, author1_.id as id1_0_1_, author1_.fullName as fullName2_0_1_ from BookFetchModeJoin_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookFetchModeJoin_id=?
HQL запрос с «join fetch»
Чтобы корневые сущности со связанными коллекциями были загружены в одном SQL запросе, используйте оператор JOIN FETCH
. Результатом запроса будет декартово произведение. Вместо 4 элементов в результирующей выборке, запрос вернет 6, потому что книги «Enterprise Integration Patterns» и «NoSQL» имеют двух авторов и будут дважды встречаться в результатах запроса. Коллекции, которые не были присоединены оператором JOIN FETCH
, будут загружены согласно типу и режиму загрузки.
В данном примере будет выполнено по дополнительному запросу на каждую корневую сущность из списка результатов для загрузки списка сущностей Category
.
List books = getCurrentSession().createQuery("select b from BookFetchModeJoin b join fetch b.authors a").list(); assertEquals(6, books.size());
Сгенерированный SQL:
select bookfetchm0_.id as id1_7_0_, author2_.id as id1_0_1_, bookfetchm0_.isbn as isbn2_7_0_, bookfetchm0_.publicationDate as publicat3_7_0_, bookfetchm0_.title as title4_7_0_, author2_.fullName as fullName2_0_1_, authors1_.BookFetchModeJoin_id as BookFetc1_8_0__, authors1_.authors_id as authors_2_8_0__ from BookFetchModeJoin bookfetchm0_ inner join BookFetchModeJoin_Author authors1_ on bookfetchm0_.id=authors1_.BookFetchModeJoin_id inner join Author author2_ on authors1_.authors_id=author2_.id select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=? select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=? select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=? select categories0_.BookFetchModeJoin_id as BookFetc1_9_0_, categories0_.categories_id as categori2_9_0_, category1_.id as id1_16_1_, category1_.description as descript2_16_1_, category1_.name as name3_16_1_ from BookFetchModeJoin_Category categories0_ inner join Category category1_ on categories0_.categories_id=category1_.id where categories0_.BookFetchModeJoin_id=?
Hibernate Criteria запрос и FetchMode.JOIN
В Hibernate Criteria запросах связанные коллекции с режимом загрузки FetchMode.JOIN
будут загружены сразу (тип загрузки EAGER
) и в одном запросе с корневыми сущностями при помощи SQL оператора JOIN
. Как уже рассматривалось в прошлой статье, только одна коллекций, которая загружается со стратегией JOIN
может быть типа java.util.List
, остальные коллекции, которые загружаются стратегией JOIN
должны быть типа java.util.Set
, иначе будет выброшено исключение org.hibernate.loader.MultipleBagFetchException
.
List books = getCurrentSession().createCriteria(BookFetchModeJoin.class).list();
Будет выброшено исключение:org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
HQL и Hibernate Criteria запросы и FetchMode.SELECT с @BatchSize
@BatchSize
устанавливает количество коллекций, которые должны быть загружены в одном SQL запросе. Если результат запроса содержит 4 сущности, каждая из которых имеет по связанной коллекции, при режим загрузки SELECT
будет выполнено 5 запросов. Один запрос для загрузки всех корневых сущностей и по одному для загрузки связанной коллекции каждой из 4 корневых сущностей. @BatchSize(size = 2)
указывает ORM загружать по 2 связанные коллекции в одном запросе. Таким образом, всего будет выполнено 3 запроса вместо 5. Один запрос для загрузки всех корневых сущностей и еще 2 запроса, каждый из которых загрузит по 2 связанные коллекции для 2 корневых сущностей.
List books = getCurrentSession().createQuery("select b from BookBatchSize b").list(); assertEquals(4, books.size());
Как уже отмечалось ранее, при режиме загрузки FetchMode.SELECT
HQL и Hibernate Criteria запросы ведут себя одинаково. При использовании @BatchSize
поведение также будет одинаковым.
List books = getCurrentSession().createCriteria(BookBatchSize.class).list(); assertEquals(4, books.size());
Сгенерированный SQL:
select bookbatchs0_.id as id1_4_, bookbatchs0_.isbn as isbn2_4_, bookbatchs0_.publicationDate as publicat3_4_, bookbatchs0_.title as title4_4_ from BookBatchSize bookbatchs0_ select authors0_.BookBatchSize_id as BookBatc1_5_1_, authors0_.authors_id as authors_2_5_1_, author1_.id as id1_0_0_, author1_.fullName as fullName2_0_0_ from BookBatchSize_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookBatchSize_id in ( ?, ? ) select authors0_.BookBatchSize_id as BookBatc1_5_1_, authors0_.authors_id as authors_2_5_1_, author1_.id as id1_0_0_, author1_.fullName as fullName2_0_0_ from BookBatchSize_Author authors0_ inner join Author author1_ on authors0_.authors_id=author1_.id where authors0_.BookBatchSize_id in ( ?, ? )
Выводы
Существует немало нюансов, связанных со стратегиями загрузки связанных коллекций в JPA и Hibernate, многие из которых были рассмотрены в этой и предыдущейстатьях.
Не стоит использовать одну выбранную стратегию загрузки повсюду в приложении. Каждый случай надо проанализировать индивидуально и выбрать оптимальную стратегию загрузки. Как это часто бывает, в большинстве случаев стратегия загрузки по умолчанию будет оптимальным вариантом. Если приложение испытывает проблемы с производительностью, которые вызваны неправильным выбором стратегии загрузки, для анализа пригодятся следующие свойства Hibernate конфигурации: hibernate.show_sql=true, hibernate.format_sql=true и hibernate.use_sql_comments=true
.