Русский
Русский
English
Статистика
Реклама

Перевод Проблема с N1 запросами в JPA и Hibernate

В преддверии курса "Highload Architect" приглашаем вас посетить открытый урок по теме "Паттерны горизонтального масштабирования хранилищ".


А пока делимся традиционным переводом полезной статьи.


Введение

В этой статье я расскажу, в чем состоит проблема N + 1 запросов при использовании JPA и Hibernate, и как ее лучше всего исправить.

Проблема N + 1 не специфична для JPA и Hibernate, с ней вы можете столкнуться и при использовании других технологий доступа к данным.

Что такое проблема N + 1

Проблема N + 1 возникает, когда фреймворк доступа к данным выполняет N дополнительных SQL-запросов для получения тех же данных, которые можно получить при выполнении одного SQL-запроса.

Чем больше значение N, тем больше запросов будет выполнено и тем больше влияние на производительность. И хотя лог медленных запросов может вам помочь найти медленные запросы, но проблему N + 1 он не обнаружит, так как каждый отдельный дополнительный запрос выполняется достаточно быстро.

Проблема заключается в выполнении множества дополнительных запросов, которые в сумме выполняются уже существенное время, влияющее на быстродействие.

Рассмотрим следующие таблицы БД: post (посты) и post_comments (комментарии к постам), которые связаны отношением "один-ко-многим":

Вставим в таблицу post четыре строки:

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)

А в таблицу 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-запроса:

List<Tuple> comments = entityManager.createNativeQuery("""    SELECT        pc.id AS id,        pc.review AS review,        pc.post_id AS postId    FROM post_comment pc    """, Tuple.class).getResultList();

А позже решите получить заголовок (title) связанного поста (post) для каждого комментария (post_comment):

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-запроса вы выполнили пять (1 + 4):

SELECT    pc.id AS id,    pc.review AS review,    pc.post_id AS postIdFROM 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-запросом, например, так:

List<Tuple> comments = 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 и Hibernate

При использовании JPA и Hibernate есть несколько способов получить проблему N + 1, поэтому очень важно знать, как избежать таких ситуаций.

Рассмотрим следующие классы, которые мапятся на таблицы post и 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.EAGER

Использование явного или неявного FetchType.EAGER для JPA-ассоциаций плохая идея, потому что будет загружаться гораздо больше данных, чем вам нужно. Более того, стратегия FetchType.EAGER также подвержена проблемам N + 1.

К сожалению, ассоциации @ManyToOne и @OneToOne по умолчанию используют FetchType.EAGER, поэтому, если ваши маппинги выглядят следующим образом:

@ManyToOneprivate Post post;

У вас используется FetchType.EAGER и каждый раз, когда вы забываете указатьJOIN FETCH при загрузке сущностей PostComment с помощью JPQL-запроса или Criteria API:

List<PostComment> comments = 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 = 1SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

Обратите внимание на дополнительные запросы SELECT, которые появились, потому что перед возвращением списка сущностей PostComment необходимо извлечь ассоциацию с post.

В отличие от значений по умолчанию, используемых в методе find из EntityManager, в JPQL-запросах и Criteria API явно указывается план выборки (fetch plan), который Hibernate не может изменить, автоматически применив JOIN FETCH. Таким образом, вам это нужно делать вручную.

Если вам совсем не нужна ассоциация с post, то не повезло: с использованием FetchType.EAGER нет способа избежать ее получения. Поэтому по умолчанию лучше использовать FetchType.LAZY.

Но если вы хотите использовать ассоциацию с post, то можно использовать JOIN FETCH, чтобы избежать проблемы с N + 1:

List<PostComment> comments = 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 pcINNER 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.EAGER, читайте в этой статье.

FetchType.LAZY

Даже если вы явно перейдете на использование FetchType.LAZY для всех ассоциаций, то вы все равно можете столкнуться с проблемой N + 1.

На этот раз ассоциация с post мапится следующим образом:

@ManyToOne(fetch = FetchType.LAZY)private Post post;

Теперь, когда вы запросите PostComment:

List<PostComment> comments = 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

Но если позже вы обратитесь к этой lazy-load ассоциации с post:

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:

List<PostComment> comments = 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.EAGER, этот JPQL-запрос будет генерировать один SQL-запрос.

Даже если вы используете FetchType.LAZY и не ссылаетесь на дочерние ассоциации двунаправленного отношения @OneToOne, вы все равно можете получить N + 1.

Подробнее о том, как преодолеть проблему N+1 c @OneToOne-ассоциациями, читайте в этой статье.

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

Проблема N + 1 также может возникать при использовании кэша второго уровня для обработки коллекций или результатов запроса.

Например, если выполните следующий JPQL-запрос, использующий кэш запросов:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    order by pc.post.id desc    """, PostComment.class).setMaxResults(10).setHint(QueryHints.HINT_CACHEABLE, true).getResultList();

Если PostComment не находится в кэше второго уровня, то будет выполнено N запросов для получения каждого отдельного PostComment:

-- 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 pcWHERE 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 pcWHERE 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 pcWHERE pc.id = 1

В кэше запросов хранятся только идентификаторы сущностей PostComment. Таким образом, если сущности PostComment не находятся в кэше, они будут извлечены из базы данных и вы получите N дополнительных SQL-запросов.


Подробнее о курсе "Highload Architect".

Источник: habr.com
К списку статей
Опубликовано: 24.11.2020 20:17:07
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании otus. онлайн-образование

Высокая производительность

Программирование

Sql

Jpa

Hibernate

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru