Главная Новости

Hibernate и проблема запросов N+1

Опубликовано: 24.10.2023

Hibernate и проблема запросов N+1

В предыдущей статье я писал о «Три вещи, которые вам следует знать о Hibernate». Сегодня я подробно опишу первый из них, т.е. проблему п+1 запросы.

Эта проблема касается загрузки коллекций, связанных с заданной сущностью (обычно «один ко многим»). Это очень основная проблема, потому что отношения один ко многим, является одним из наиболее часто используемых отношений.

Что вызывает проблему N + 1?

Много лет назад, когда я начал свое приключение с Hibernate, я наткнулся на примерно такое утверждение:

“С помощью Hibernate вы можете создавать приложения на базе баз данных, не зная SQL и баз данных”.

Это неправда.Без базовых знаний баз данных и SQL трудно создавать хорошо функционирующие приложения.

Программисты, которые очень верят в то, что инструмент сделает все за них и сделает это «правильно», часто попадают в ловушку N+1. Наиболее распространенными причинами этой проблемы являются:незнаниеи/илиневнимательностьпри выполнении задач (это причины, с которыми я часто сталкиваюсь).

Это показывает определенную взаимосвязь: недостаточно знать, как использовать инструмент, нужно также знать детали его реализации и держать их в глубине души.

Каковы последствия?

Эта проблема приводит крезкому снижению производительностив результате создания очень большого количества SQL-запросов. В зависимости от ситуации, таких запросов может быть несколько или даженесколько тысяч. Обычно это не является большой проблемой, если это происходит в вашей среде разработки, поскольку обычно в ней имеется лишь небольшой объем данных, поэтому это трудно даже заметить. Однако все становится сложнее, когда приложение запускается в производственной среде, где обычно имеется гораздо больший объем"тестовых"данных. А также когда приложением одновременно пользуются хотя бы несколько пользователей. Поэтому стоит подготовить тестовую среду, хотя бы частично близкую к производственной.

Как обнаружить проблему запроса N+1?

Самый простой способ обнаружить эту проблему — зарегистрировать SQL-запросы, выполняемые Hibernate (помните, что это не рекомендуется в рабочей среде). Для этой цели используются свойства show_sql. В примере приложения я буду использоватьSpring BootиSpring Data,, а в apication.properties можно установить следующие свойства:

Spring.jpa.show-sql=истина

или в качестве параметра при запуске приложения:

--spring.jpa.show-sql=истина

Вы также можете включить статистику сеансов Спящий режим : :

Spring.jpa.properties.hibernate.generate_statistics = true

Пример реализации

Предположим, нам нужно получить из базы данных список всех пользователей с их адресами. У нас в базе 10 пользователей и у каждого из них по 5 адресов.

Сущность Пользователь представляет нашего пользователя и содержит связанный список адресов:

@Entity  public class User {  @Id  @GeneratedValue  частный длинный идентификатор; личное имя пользователя String; @OneToMany  @JoinColumn(name = "userId")  частные адреса List; //... геттеры и сеттеры }

Сущность Адрес представляет адрес пользователя:

@Entity  адрес общедоступного класса {  @Id  @GeneratedValue  частный длинный идентификатор; частный длинный идентификатор пользователя; частная Струнная улица; частный строковый почтовый индекс; частный Стринг-Сити; //... геттеры и сеттеры }

Когда мы пытаемся получить список пользователей с помощью Hibernate, Hibernate по умолчанию получает только список пользователей (1 расследование). Если во время сеанса Hibernate мы попытаемся получить список адресов всех пользователей (с помощью метода user.getAddresses()), Hibernate дополнительно выполнит один запрос для каждого пользователя (н - запросы, н это количество пользователей). В этой ситуации Hibernate выполнит 11 запросов.

Пример запросаJPQL(Java Persistence Query Language) для списка пользователей будет выглядеть следующим образом:

выбрать тебя из пользователя u

ВSpring Dataнам просто нужно использовать метод найти все из пользовательского репозитория. Этот метод выполняет тот же запрос, что и выше. Благодаря данным Spring нам не нужно предоставлять их явно.

Контроллер позаботится о предоставлении этим пользователям:

@RestController public class NusOneController {   Private Final NusOneUserRepository nusOneUserRepository; public NusOneController(NusOneUserRepository nusOneUserRepository) {  this.nusOneUserRepository = nusOneUserRepository; }   @GetMapping("/np1/users")  public List getUsers() {  return nusOneUserRepository.findAll(); } }

Чтобы вызвать такую ​​службу, просто запустите в консоли команду:

локон -s локальный хост: 8080/np1/пользователей

Как видно из журнала, Hibernate выполняет11запросов:

Как решить проблему запросов N+1?

Используйте пакетный размер

Есть несколько решений, первое — установить аннотации @Размер партии с соответствующим выбранным параметром размер.

@OneToMany @JoinColumn(name = "userId") @BatSize(size = 5) адреса частного списка;

Это приведет к загрузке связанных объектов в видекусков. Например, если у нас 10 пользователей и у каждого из них по 5 адресов (т.е. у нас в базе 50 адресов), то для получения всех адресов этих пользователей нам понадобится2 дополнительных запроса[(14505 )] (вместо 10 предыдущих). Настройка размер для 10 и более это будет1 дополнительный запрос.

Это не идеальное решение, но в некоторых случаях оно будет работать идеально.

Чтобы вызвать службу, представляющую этот пример, просто запустите:

завиток -s локальный хост: 8080/bs/пользователей

С этой настройкой Hibernate выполняеттолько 3 запроса:

Используйте запрос JPQL вместе с Join Fet

Другое решение — получить пользователей и адресав одном запросе, используя Присоединяйтесь к выборке: :

выберите вас из JoinFetUser и присоединитесь к fet u.addresses

Кроме того, мы должны обеспечить уникальность результатов. Мы делаем это потому, что Hibernate преобразует такой запрос в выборку с объединением, которая вернет 50 строк результатов, а затем преобразует его в список из 50 пользователей с адресами.

Обеспечить уникальность результатов можно двумя способами:

  1. Они используют в запросе ключевое словоdistinct(нам придется переопределить метод найти все из пользовательского репозитория)
  2. Вместо возврата списка пользователей мы возвращаем Набор (нам придется создать новый метод в пользовательском репозитории)

Я буду использовать ключевое слово отчетливый.

выберите отдельный u из JoinFetUser и присоединитесь к выборке u.addresses

И ниже приведен перезаписанный метод найти все: :

@Override @Query("выберите отдельный u из JoinFetUser и присоединитесь к выборке u.addresses") List findAll();

В этом случае мы извлекаем всех пользователей с адресами (одним sql-запросом), что в большинстве случаев является вполне хорошим и эффективным решением.

Это рекомендуемое решениеэтой проблемы.

Чтобы вызвать службу представления, просто запустите этот пример:

локон -s локальный хост: 8080/jf/пользователей

Для такого запроса JPQL Hibernate генерирует следующий запрос выбора с соединением:

В видео ниже я представляю, среди прочего: какN+1работает вHibernateпри использованииSpring Data. Я также покажу вам два способа избавиться от этого.

Краткое содержание

В этой статье я постарался подробно познакомить вас с проблемой n+1 и способами ее решения. Надеюсь, мне удалось пролить свет на эту проблему. Исходные коды тестового приложения можно найти на github: Пример спящего режима.

rss