Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье я собираюсь объяснить, в чем проблема с запросами N+1 при использовании JPA и Hibernate и как ее лучше всего исправить.
Проблема с запросом N+1 не относится к JPA и Hibernate, так как вы можете столкнуться с этой проблемой, даже если используете другие технологии доступа к данным.
В чем проблема с запросом N+1
Проблема с запросом N+1 возникает, когда платформа доступа к данным выполняет N дополнительных инструкций SQL для извлечения тех же данных, которые могли быть получены при выполнении основного запроса SQL.
Чем больше значение N, тем больше запросов будет выполнено, тем больше влияние на производительность. И, в отличие от журнала медленных запросов, который может помочь вам найти медленно выполняемые запросы, проблема+1 не будет обнаружена, потому что каждый отдельный дополнительный запрос выполняется достаточно быстро, чтобы не вызвать журнал медленных запросов.
Проблема заключается в выполнении большого количества дополнительных запросов, которые в целом занимают достаточно времени, чтобы замедлить время отклика.
Давайте рассмотрим, что у нас есть следующие таблицы post
и post_comments
базы данных, которые образуют отношение один ко многим таблицам :
Мы собираемся создать следующие 4 записи
строки:
INSERT INTO post (title, id) VALUES ('High-Performance Java Persistence - Part 1', 1) INSERT INTO post (title, id) VALUES ('High-Performance Java Persistence - Part 2', 2) INSERT INTO post (title, id) VALUES ('High-Performance Java Persistence - Part 3', 3) INSERT INTO post (title, id) VALUES ('High-Performance Java Persistence - Part 4', 4)
И мы также создадим 4 post_comment
дочерние записи:
INSERT INTO post_comment (post_id, review, id) VALUES (1, 'Excellent book to understand Java Persistence', 1) INSERT INTO post_comment (post_id, review, id) VALUES (2, 'Must-read for Java developers', 2) INSERT INTO post_comment (post_id, review, id) VALUES (3, 'Five Stars', 3) INSERT INTO post_comment (post_id, review, id) VALUES (4, 'A great reference book', 4)
Проблема с запросом N+1 с простым SQL
Как уже объяснялось, проблема с запросом N+1 может быть вызвана с помощью любой технологии доступа к данным, даже с помощью обычного SQL.
Если вы выберете post_comments
с помощью этого SQL – запроса:
Listcomments = entityManager.createNativeQuery(""" SELECT pc.id AS id, pc.review AS review, pc.post_id AS postId FROM post_comment pc """, Tuple.class) .getResultList();
И позже вы решите выбрать соответствующий пост
заголовок для каждого пост_коммент
:
for (Tuple comment : comments) { String review = (String) comment.get("review"); Long postId = ((Number) comment.get("postId")).longValue(); String postTitle = (String) entityManager.createNativeQuery(""" SELECT p.title FROM post p WHERE p.id = :postId """) .setParameter("postId", postId) .getSingleResult(); LOGGER.info( "The Post '{}' got this review '{}'", postTitle, review ); }
Вы собираетесь вызвать проблему с запросом N+1, потому что вместо одного SQL-запроса вы выполнили 5 (1 + 4):
SELECT pc.id AS id, pc.review AS review, pc.post_id AS postId FROM post_comment pc SELECT p.title FROM post p WHERE p.id = 1 -- The Post 'High-Performance Java Persistence - Part 1' got this review -- 'Excellent book to understand Java Persistence' SELECT p.title FROM post p WHERE p.id = 2 -- The Post 'High-Performance Java Persistence - Part 2' got this review -- 'Must-read for Java developers' SELECT p.title FROM post p WHERE p.id = 3 -- The Post 'High-Performance Java Persistence - Part 3' got this review -- 'Five Stars' SELECT p.title FROM post p WHERE p.id = 4 -- The Post 'High-Performance Java Persistence - Part 4' got this review -- 'A great reference book'
Исправить проблему с запросом N+1 очень просто. Все, что вам нужно сделать, это извлечь все необходимые данные из исходного SQL-запроса, например:
Listcomments = entityManager.createNativeQuery(""" SELECT pc.id AS id, pc.review AS review, p.title AS postTitle FROM post_comment pc JOIN post p ON pc.post_id = p.id """, Tuple.class) .getResultList(); for (Tuple comment : comments) { String review = (String) comment.get("review"); String postTitle = (String) comment.get("postTitle"); LOGGER.info( "The Post '{}' got this review '{}'", postTitle, review ); }
На этот раз выполняется только один SQL-запрос для извлечения всех данных, которые нас интересуют в дальнейшем.
Проблема с запросом N+1 с JPA и гибернацией
При использовании JPA и гибернации существует несколько способов вызвать проблему с запросом N+1, поэтому очень важно знать, как избежать этих ситуаций.
Для следующих примеров рассмотрим, что мы сопоставляем сообщение
и post_comments
таблицы для следующих объектов:
Сопоставления JPA выглядят следующим образом:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id private Long id; private String title; //Getters and setters omitted for brevity } @Entity(name = "PostComment") @Table(name = "post_comment") public class PostComment { @Id private Long id; @ManyToOne private Post post; private String review; //Getters and setters omitted for brevity }
Тип выборки. ЖАЖДУЩИЙ
Используя FetchType. НЕТЕРПЕЛИВЫЙ
либо неявно, либо явно для ваших ассоциаций JPA-плохая идея, потому что вы собираетесь получить гораздо больше данных, которые вам нужны. Более того, тип FetchType. НЕТЕРПЕЛИВАЯ
стратегия также подвержена проблемам с запросами N+1.
К сожалению, @manytoon
и @OneToOne
ассоциации используют Тип выборки. НЕТЕРПЕЛИВЫЙ
по умолчанию, итак, если ваши сопоставления выглядят так:
@ManyToOne private Post post;
Вы используете тип FetchType. НЕТЕРПЕЛИВАЯ
стратегия, и каждый раз, когда вы забываете использовать ПРИСОЕДИНИТЬСЯ к ВЫБОРКЕ
при загрузке некоторых Комментариев
сущностей с запросом JPQL или API критериев:
Listcomments = entityManager .createQuery(""" select pc from PostComment pc """, PostComment.class) .getResultList();
Вы собираетесь вызвать проблему с запросом N+1:
SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_ FROM post_comment pc SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
Обратите внимание на дополнительные инструкции SELECT, которые выполняются, поскольку сообщение
ассоциация должна быть извлечена до возврата Списка
объектов PostComment|/.
В отличие от плана выборки по умолчанию, который вы используете при вызове find
метода EntityManager
, запрос JPQL или API критериев определяет явный план, который Hibernate не может изменить, автоматически вводя ВЫБОРКУ ОБЪЕДИНЕНИЯ. Итак, вам нужно сделать это вручную.
Если вам вообще не нужна была ассоциация post
, вам не повезло при использовании FetchType. НЕТЕРПЕЛИВЫЙ
потому что нет никакого способа избежать его получения. Вот почему лучше использовать FetchType. ЛЕНИВЫЙ
по умолчанию.
Но, если вы хотите использовать post
ассоциацию, то вы можете использовать JOIN FETCH
, чтобы избежать проблемы с запросом N+1:
Listcomments = entityManager.createQuery(""" select pc from PostComment pc join fetch pc.post p """, PostComment.class) .getResultList(); for(PostComment comment : comments) { LOGGER.info( "The Post '{}' got this review '{}'", comment.getPost().getTitle(), comment.getReview() ); }
На этот раз Hibernate выполнит одну инструкцию SQL:
SELECT pc.id as id1_1_0_, pc.post_id as post_id3_1_0_, pc.review as review2_1_0_, p.id as id1_0_1_, p.title as title2_0_1_ FROM post_comment pc INNER JOIN post p ON pc.post_id = p.id -- The Post 'High-Performance Java Persistence - Part 1' got this review -- 'Excellent book to understand Java Persistence' -- The Post 'High-Performance Java Persistence - Part 2' got this review -- 'Must-read for Java developers' -- The Post 'High-Performance Java Persistence - Part 3' got this review -- 'Five Stars' -- The Post 'High-Performance Java Persistence - Part 4' got this review -- 'A great reference book'
Для получения более подробной информации о том, почему вам следует избегать FetchType. НЕТЕРПЕЛИВЫЙ
стратегия извлечения, ознакомьтесь также с этой статьей .
Тип выборки. ЛЕНИВЫЙ
Даже если вы переключитесь на использование FetchType. ЛЕНИВЫЙ
явно для всех ассоциаций, вы все равно можете столкнуться с проблемой N+1.
На этот раз ассоциация post
отображается следующим образом:
@ManyToOne(fetch = FetchType.LAZY) private Post post;
Теперь, когда вы получаете Комментарий к сообщению
сущности:
Listcomments = entityManager .createQuery(""" select pc from PostComment pc """, PostComment.class) .getResultList();
Hibernate выполнит одну инструкцию SQL:
SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_ FROM post_comment pc
Но, если после этого вы собираетесь ссылаться на лениво загруженную публикацию
ассоциацию:
for(PostComment comment : comments) { LOGGER.info( "The Post '{}' got this review '{}'", comment.getPost().getTitle(), comment.getReview() ); }
Вы получите проблему с запросом N+1:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1 -- The Post 'High-Performance Java Persistence - Part 1' got this review -- 'Excellent book to understand Java Persistence' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2 -- The Post 'High-Performance Java Persistence - Part 2' got this review -- 'Must-read for Java developers' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3 -- The Post 'High-Performance Java Persistence - Part 3' got this review -- 'Five Stars' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4 -- The Post 'High-Performance Java Persistence - Part 4' got this review -- 'A great reference book'
Поскольку ассоциация post
извлекается лениво, при обращении к ленивой ассоциации будет выполняться дополнительная инструкция SQL для создания сообщения журнала.
Опять же, исправление состоит в добавлении предложения JOIN FETCH
в запрос JPQL:
Listcomments = entityManager.createQuery(""" select pc from PostComment pc join fetch pc.post p """, PostComment.class) .getResultList(); for(PostComment comment : comments) { LOGGER.info( "The Post '{}' got this review '{}'", comment.getPost().getTitle(), comment.getReview() ); }
И, точно так же, как в FetchType. Например, этот JPQL-запрос создаст одну инструкцию SQL.
Даже если вы используете FetchType. ЛЕНИВЫЙ
и не ссылайтесь на дочернюю ассоциацию двунаправленной связи @OneToOne
JPA, вы все равно можете вызвать проблему с запросом N+1.
Для получения более подробной информации о том, как вы можете преодолеть их+1 проблема с запросом, созданная @OneToOne
ассоциациями, ознакомьтесь с этой статьей .
Кэш второго уровня
Проблема с запросом N+1 также может быть вызвана при использовании кэша второго уровня для извлечения коллекций или результатов запроса.
Например, если вы выполните следующий JPQL-запрос, в котором используется кэш запросов:
Listcomments = entityManager.createQuery(""" select pc from PostComment pc order by pc.post.id desc """, PostComment.class) .setMaxResults(10) .setHint(QueryHints.HINT_CACHEABLE, true) .getResultList();
Если Комментарий к сообщению
не хранится в кэше второго уровня, будет выполнено N запросов для извлечения каждого отдельного сообщения
ассоциации:
-- Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache -- Checking query spaces are up-to-date: [post_comment] -- [post_comment] last update timestamp: 6244574473195524, result set timestamp: 6244574473207808 -- Returning cached query results SELECT pc.id AS id1_1_0_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_ FROM post_comment pc WHERE pc.id = 3 SELECT pc.id AS id1_1_0_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_ FROM post_comment pc WHERE pc.id = 2 SELECT pc.id AS id1_1_0_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_ FROM post_comment pc WHERE pc.id = 1
В кэше запросов хранятся только идентификаторы сущностей соответствующих Комментариев к сообщению
сущностей. Таким образом, если объекты Post Comment
также не кэшируются, они будут извлечены из базы данных. Следовательно, вы получите N дополнительных инструкций SQL.
Для получения более подробной информации об этой теме ознакомьтесь с этой статьей .
Вывод
Знание проблемы с запросом N+1 очень важно при использовании любой платформы доступа к данным, а не только JPA или Hibernate.
В то время как для запросов сущностей, таких как JPQL или API критериев, ВЫБОРКА JOIN
предложение – лучший способ избежать проблемы с запросом N+1, для кэша запросов вам нужно будет убедиться, что базовые сущности хранятся в кэше.
Если вы хотите автоматически обнаружить проблему с запросом N+1 на уровне доступа к данным, в этой статье объясняется, как это можно сделать с помощью проекта db-util
с открытым исходным кодом.