Статьи по Java

Проблема n + 1 в Hibernate

Hibernate – это очень популярный ORM фреймворк, предназначенный для программ, написанных на Java или других языках, работающих на виртуальной машине Java, таких как Kotlin.

Такие инструменты позволяют двунаправленно отображать реляционный мир баз данных на мир объектов. Популярность реляционных баз данных как хранилищ отражается в популярности ORM-решений.

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

Одной из таких известных проблем является проблема названия n + 1, которая рассматривается в этой заметке.

Проблема n + 1

Проблема n + 1 может возникнуть в случае, когда одна сущность (таблица) ссылается на другую сущность (таблицу).

В такой ситуации получается, что для получения значения зависимой сущности выполняется n избыточных запросов, в то время как достаточно одного.

Никого не нужно убеждать, что это негативно влияет на производительность системы и создает ненужную нагрузку на базу данных. Особенно то, что количество запросов увеличивается с ростом n.

Сама проблема часто представляется как возникающая только в отношениях “один ко многим” (javax.persistence.OneToMany) или только в случае ленивой загрузки данных (javax.persistence.FetchType.LAZY). Это не так, и следует помнить, что эта проблема также может возникнуть в отношениях один-к-одному и при “жадной” загрузке зависимых сущностей.

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

class Storage { fun getBoxes(limit: Int): List<Box> { return getEntityManager() .createQuery("select b from Box b order by id", Box::class.java) .setMaxResults(limit) .resultList } private fun getEntityManager(): EntityManager { return Persistence.createEntityManagerFactory("persistence") .createEntityManager() } } fun main(args: Array<String>) { val storage = Storage() println(storage.getBoxes(4)) }
Code language: PHP (php)

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

Здесь мы видим отношения “один ко многим” между таблицами. Используя JPA (Java Persistence API) в реализации Hibernate, мы можем написать это на Kotlin следующим образом:

@Table(name = "box") @Entity data class Box( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Int, @Column(name = "name", length = 50, nullable = false) val name: String, @OneToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL]) @JoinColumn(name = "box_id") val toys: List<Toy> ) @Table(name = "toy") @Entity data class Toy( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Int, @Column(name = "name", length = 50, nullable = false) val name: String )
Code language: JavaScript (javascript)

Выполним запрос из функции main для получения четырех ящиков из базы данных и посмотрим на запросы, выполненные Hibernate:

select box.id, box.name from box order by box.id limit 4 select toy.box_id, toy.id, toy.name from toy where toy.box_id=4 select toy.box_id, toy.id, toy.name from toy where toy.box_id=2 select toy.box_id, toy.id, toy.name from toy where toy.box_id=1 select toy.box_id, toy.id, toy.name from toy where toy.box_id=3
Code language: JavaScript (javascript)

Очевидно, что для получения четырех ящиков было выполнено 4 + 1 запросов. Сначала Hibernate извлек любые четыре коробки, затем для каждой коробки выполнил один запрос, чтобы извлечь принадлежащие ей игрушки.

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

fun getBoxes(limit: Int): List<Box> { return getEntityManager() .createQuery("select b from Box b join fetch b.toys order by b.id", Box::class.java) .setMaxResults(limit) .resultList }
Code language: PHP (php)

Теперь количество запросов, отправляемых к базе данных, сократилось до одного:

select box.id, toy.id, box.name, toy.name, toy.box_id, from box inner join toy on box.id=toy.box_id order by box.id
Code language: JavaScript (javascript)

Однако составление собственных запросов не всегда является лучшим способом выполнения такого рода задач. Что если мы используем, например, репозитории, предоставляемые Spring Data?

Существует и другой подход к решению этой проблемы, а именно использование аннотации org.hibernate.annotations.BatchSize, которую можно найти в ORM-библиотеке Hibernate ORM Hibernate Core.

Мы применим эту аннотацию, поместив ее над полем для игрушек:

@Table(name = "box") @Entity data class Box( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Int, @Column(name = "name", length = 50, nullable = false) val name: String, @OneToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL]) @JoinColumn(name = "box_id") @BatchSize(size = 256) val toys: List<Toy> )
Code language: JavaScript (javascript)

Добавив аннотацию @BatchSize над полем игрушек, Hibernate получит данные об игрушках, назначенных по ячейкам в “партиях”. (пакетный), т.е. для 256 экземпляров Box, Hibernate получит их игрушки в рамках одного запроса. Рассмотрим запросы, которые были сгенерированы для первой версии функции getBoxes:

select box.id, box.name from box order by box.id limit 4 select toy.box_id, toy.id, toy.name from toy where toy.box_id in (4, 1, 2, 3)
Code language: CSS (css)

Неоспоримо, что второй запрос извлекает всю “партию” ящиков. Если бы размер пакета был ограничен двумя (@BatchSize(size = 2)), мы бы увидели два запроса, каждый из которых извлекает по два элемента на пакет:

select box.id, box.name from box order by box.id limit 4 select toy.box_id, toy.id, toy.name from toy where toy.box_id in (4, 1) select toy.box_id, toy.id, toy.name from toy where toy.box_id in (2, 3)
Code language: CSS (css)

Резюме

Отношения “один к одному” или “один ко многим” вполне естественны в реляционных системах баз данных. По этой причине в приложениях, использующих Hibernate, нередко возникает эта проблема.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *