| April 25, 2024
Пример N+1 проблемы
Проще всего проблему проиллюстрировать используя классический пример с Книгой (book) и Автором (author).
Предположим, что у нас в модели есть сущность Book
. У каждой сущности Book
есть свой Author
.
В Java, с использованием Hibernate это отношение можно смоделировать следующим образом - класс Book:
@Data
@Entity
@Table(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
private String title;
@OneToOne(fetch = FetchType.LAZY)
private Author author;
}
Класс Author:
@Data
@Entity
@Table(name = "author")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
private String name;
@OneToOne(mappedBy = "author")
private Book book;
}
Поле book
в классе Author
указывает на то, что владельцем отношения является сущность Book
:
@OneToOne(mappedBy = "author")
private Book book;
Предположим, что нам необходимо загрузить из базы данных несколько книг. Для доступа к базе данных, мы используем простейший Spring Repository:
public interface BookRepository extends CrudRepository<Book, Integer> {
}
C репозиторием, подобным тому, что приведён выше, уже можно зачитать из базы данных все сохранённые Книги (book). Сделать это можно, просто вызвав следующий метод:
repository.findAll();
При наличии следующей конфигурации:
spring:
jpa:
show-sql: true
В логах приложения мы увидим:
Hibernate: select book0_.id as id1_1_, book0_.author_id as author_i3_1_, book0_.title as title2_1_ from book book0_
Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, book1_.id as id1_1_1_, book1_.author_id as author_i3_1_1_, book1_.title as title2_1_1_
from author author0_ left outer join book book1_ on author0_.id=book1_.author_id where author0_.id=?
Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, book1_.id as id1_1_1_, book1_.author_id as author_i3_1_1_, book1_.title as title2_1_1_
from author author0_ left outer join book book1_ on author0_.id=book1_.author_id where author0_.id=?
Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, book1_.id as id1_1_1_, book1_.author_id as author_i3_1_1_, book1_.title as title2_1_1_
from author author0_ left outer join book book1_ on author0_.id=book1_.author_id where author0_.id=?
Разберём подробнее, что же в мы тут видим.
Первый запрос:
select book0_.id as id1_1_, book0_.author_id as author_i3_1_, book0_.title as title2_1_ from book book0_
Зачитывает все имеющиеся записи в таблице book
. Для своего эксперимента я сохранил 3 сущности типа Book
.
У каждой из этих сущностей, был установлен свой, уникальный автор - Author
. Итого, в базе данных сохранено 3 записи в таблице book
и 3 записи в таблице author
.
После того как Hibernate зачитал все имеющиеся книги одним запросом, он начал зачитывать записи из таблицы с авторами по одному, с использованием outer join конструкции.
Это и есть проблема - N+1.
Что такое N+1?
Проблема N+1 - это проблема неоптимального доступа к сохранённым данным, при которой каждой из записей зачитанных одним sql запросом связанные данные будут зачитаны по одному.
N+1 можно так же расшифровать как - один запрос, чтобы зачитать множество сущностей одного типа, но N запросов, чтобы зачитать все связанные сущности.
Почему это происходит?
Потому что, в момент когда мы используем репозиторий для загрузки всех сущностей типа Book
, мы не сообщаем о том,
что нам так же нужны связанные сущности типа Author
.
Запрос за каждой связанной сущностью Author
происходит в момент доступа к ней, через поле author
объектов типа Book
.
Как обойти проблему?
Обойти проблему можно используя нативную фичу Hibernate под названием «Batch Fetching» (пакетная выборка).
Достаточно добавить следующую аннотацию на объявление класса сущности Author
:
@BatchSize(size = 100)
И запросы в логах будут выглядеть следующим образом:
Hibernate: select book0_.id as id1_1_, book0_.author_id as author_i3_1_, book0_.title as title2_1_ from book book0_
Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_, book1_.id as id1_1_1_, book1_.author_id as author_i3_1_1_, book1_.title as title2_1_1_ from author author0_ left outer join book book1_ on author0_.id=book1_.author_id where author0_.id in (?, ?, ?)
Первый запрос зачитает все записи из таблицы book
, а второй запрос зачитает все связные записи из таблицы author
.
Список материалов
- HIBERNATE - Relational Persistence for Idiomatic Java - документация о пакетной выборке данных в Hibernate