В преддверии курса "Highload Architect" приглашаем вас посетить открытый урок по теме "Паттерны горизонтального масштабирования хранилищ". А пока делимся традиционным переводом полезной статьи.  В этой статье я расскажу, в чем состоит проблема N + 1 запросов при использовании JPA и Hibernate, и как ее лучше всего исправить.  Проблема N + 1 не специфична для JPA и Hibernate, с ней вы можете столкнуться и при использовании других технологий доступа к данным. Проблема N + 1 возникает, когда фреймворк доступа к данным выполняет N дополнительных SQL-запросов для получения тех же данных, которые можно получить при выполнении одного SQL-запроса. Чем больше значение N, тем больше запросов будет выполнено и тем больше влияние на производительность. И хотя лог медленных запросов может вам помочь найти медленные запросы, но проблему N + 1 он не обнаружит, так как каждый отдельный дополнительный запрос выполняется достаточно быстро.  Проблема заключается в выполнении множества дополнительных запросов, которые в сумме выполняются уже существенное время, влияющее на быстродействие. Рассмотрим следующие таблицы БД: post (посты) и post_comments (комментарии к постам), которые связаны отношением "один-ко-многим": Вставим в таблицу post четыре строки: А в таблицу post_comment четыре дочерние записи: Как уже говорилось, проблема N + 1 может возникнуть при использовании любой технологии доступа к данным, даже при прямом использовании SQL. Если вы выберете post_comments с помощью следующего SQL-запроса: А позже решите получить заголовок (title) связанного поста (post) для каждого комментария (post_comment): Вы получите проблему N + 1, потому что вместо одного SQL-запроса вы выполнили пять (1 + 4): Исправить эту проблему с N + 1 запросом очень просто. Все, что нужно сделать, это извлечь все необходимые данные одним SQL-запросом, например, так: На этот раз выполняется только один SQL-запрос и возвращаются все данные, которые мы хотим использовать в дальнейшем. При использовании JPA и Hibernate есть несколько способов получить проблему N + 1, поэтому очень важно знать, как избежать таких ситуаций. Рассмотрим следующие классы, которые мапятся на таблицы post и post_comments: JPA-маппинг выглядят следующим образом: Использование явного или неявного FetchType.EAGER для JPA-ассоциаций — плохая идея, потому что будет загружаться гораздо больше данных, чем вам нужно. Более того, стратегия FetchType.EAGER также подвержена проблемам N + 1.  К сожалению, ассоциации @ManyToOne и @OneToOne по умолчанию используют FetchType.EAGER, поэтому, если ваши маппинги выглядят следующим образом: У вас используется FetchType.EAGER и каждый раз, когда вы забываете указать JOIN FETCH при загрузке сущностей PostComment с помощью JPQL-запроса или Criteria API: Вы сталкиваетесь с проблемой N + 1: Обратите внимание на дополнительные запросы SELECT, которые появились, потому что перед возвращением списка сущностей PostComment необходимо извлечь ассоциацию с post. В отличие от значений по умолчанию, используемых в методе find из EntityManager, в JPQL-запросах и Criteria API явно указывается план выборки (fetch plan), который Hibernate не может изменить, автоматически применив JOIN FETCH. Таким образом, вам это нужно делать вручную. Если вам совсем не нужна ассоциация с post, то не повезло: с использованием FetchType.EAGER нет способа избежать ее получения. Поэтому по умолчанию лучше использовать FetchType.LAZY. Но если вы хотите использовать ассоциацию с post, то можно использовать JOIN FETCH, чтобы избежать проблемы с N + 1: На этот раз Hibernate выполнит один SQL-запрос: Подробнее о том, почему следует избегать стратегии FetchType.EAGER, читайте в этой статье. Даже если вы явно перейдете на использование FetchType.LAZY для всех ассоциаций, то вы все равно можете столкнуться с проблемой N + 1. На этот раз ассоциация с post мапится следующим образом: Теперь, когда вы запросите PostComment: Hibernate выполнит один SQL-запрос: Но если позже вы обратитесь к этой lazy-load ассоциации с post: Вы получите проблему с N + 1 запросом: Поскольку ассоциация с post загружается лениво, при доступе к этой ассоциации будет выполняться дополнительный SQL-запрос для получения нужных данных. Опять же, решение заключается в добавлении JOIN FETCH к запросу JPQL: И как и в примере с FetchType.EAGER, этот JPQL-запрос будет генерировать один SQL-запрос. Даже если вы используете FetchType.LAZY и не ссылаетесь на дочерние ассоциации двунаправленного отношения @OneToOne, вы все равно можете получить N + 1. Подробнее о том, как преодолеть проблему N+1 c @OneToOne-ассоциациями, читайте в этой статье. Проблема N + 1 также может возникать при использовании кэша второго уровня для обработки коллекций или результатов запроса. Например, если выполните следующий JPQL-запрос, использующий кэш запросов: Если PostComment не находится в кэше второго уровня, то будет выполнено N запросов для получения каждого отдельного PostComment: В кэше запросов хранятся только идентификаторы сущностей PostComment. Таким образом, если сущности PostComment не находятся в кэше, они будут извлечены из базы данных и вы получите N дополнительных SQL-запросов. Подробнее о курсе "Highload Architect".  Редактор