Вступление
Платформы ORM, такие как JPA, упрощают процесс разработки, помогая нам избежать большого количества стандартного кода во время объектного <-> реляционного отображения данных. Тем не менее, они также приносят некоторые дополнительные проблемы к таблице, и N + 1 является одним из них. В этой статье мы кратко рассмотрим проблему, а также несколько способов ее избежать.
Проблема
В качестве примера я буду использовать упрощенную версию приложения для заказа книг онлайн. В таком приложении я мог бы создать объект, подобный приведенному ниже, для представления Заказа на покупку —
01
02
03
04
05
06
07
08
09
10
11
|
@Entity public class PurchaseOrder { @Id private String id; private String customerId; @OneToMany (cascade = ALL, fetch = EAGER) @JoinColumn (name = "purchase_order_id" ) private List<PurchaseOrderItem> purchaseOrderItems = new ArrayList<>(); } |
Заказ на покупку состоит из идентификатора заказа, идентификатора клиента и одного или нескольких покупаемых товаров. Сущность PurchaseOrderItem может иметь следующую структуру:
1
2
3
4
5
6
7
8
|
@Entity public class PurchaseOrderItem { @Id private String id; private String bookId; } |
Эти сущности были значительно упрощены, но для целей этой статьи это подойдет.
Теперь предположим, что нам нужно найти заказы клиента, чтобы отобразить их в истории заказов на покупку. Следующий запрос будет служить этой цели —
1
2
3
4
5
6
|
SELECT P FROM PurchaseOrder P WHERE P.customerId = :customerId |
который при переводе на SQL выглядит примерно так:
1
2
3
4
5
6
7
|
select purchaseor0_.id as id1_1_, purchaseor0_.customer_id as customer2_1_ from purchase_order purchaseor0_ where purchaseor0_.customer_id = ? |
Этот один запрос вернет все заказы на покупку, которые есть у клиента. Однако для получения позиций заказа JPA будет выдавать отдельные запросы для каждого отдельного заказа. Если, например, у клиента есть 5 заказов, то JPA выдаст 5 дополнительных запросов для извлечения элементов заказа, включенных в эти заказы. Это в основном известно как проблема N + 1 — 1 запрос для получения всех N заказов на покупку и N запросов для получения всех позиций заказа.
Такое поведение создает проблему масштабируемости для нас, когда наши данные растут. Даже умеренное количество заказов и предметов может создать значительные проблемы с производительностью.
Решение
Избежание нетерпеливого извлечения
Это основная причина проблемы. Мы должны избавиться от всех желающих извлечь выгоду из нашего картографирования. У них почти нет преимуществ, которые оправдывают их использование в приложениях промышленного уровня. Мы должны пометить все отношения как ленивые.
Следует отметить один важный момент — маркировка сопоставления отношений как Lazy не гарантирует, что основной постоянный поставщик также будет относиться к нему как таковому. Спецификация JPA не гарантирует ленивую выборку. В лучшем случае это подсказка постоянному провайдеру. Однако, учитывая Hibernate, я никогда не видел, чтобы он делал иначе.
Только выборка данных, которые действительно необходимы
Это всегда рекомендуется, независимо от того, решено ли вам пойти на ленивую или ленивую выборку.
Я помню одну сделанную мной оптимизацию N + 1, которая увеличила максимальное время отклика конечной точки REST с 17 минут до 1,5 секунд . Конечная точка выбирала одну сущность на основе некоторых критериев, что для нашего текущего примера будет чем-то вроде:
1
2
3
4
5
6
|
TypedQuery<PurchaseOrder> jpaQuery = entityManager.createQuery( "SELECT P FROM PurchaseOrder P WHERE P.customerId = :customerId" , PurchaseOrder. class ); jpaQuery.setParameter( "customerId" , "Sayem" ); PurchaseOrder purchaseOrder = jpaQuery.getSingleResult(); // after some calculation anotherRepository.findSomeStuff(purchaseOrder.getId()); |
Идентификатор — единственные данные из результата, которые были необходимы для последующих вычислений.
Было несколько клиентов, у которых было более тысячи заказов. У каждого из орденов в свою очередь было несколько тысяч дополнительных детей нескольких разных типов. Нет необходимости говорить, что в результате тысячи запросов выполнялись в базе данных всякий раз, когда запросы на эти заказы были получены в этой конечной точке.
Чтобы улучшить производительность, все, что я сделал, —
1
2
3
4
5
6
|
TypedQuery<String> jpaQuery = entityManager.createQuery( "SELECT P.id FROM PurchaseOrder P WHERE P.customerId = :customerId" , String. class ); jpaQuery.setParameter( "customerId" , "Sayem" ); String orderId = jpaQuery.getSingleResult(); // after some calculation anotherRepository.findSomeStuff(orderId); |
Именно это изменение привело к улучшению в 680 раз .
Если мы хотим получить более одного свойства, мы можем использовать выражение конструктора, которое предоставляет JPA:
1
2
3
4
5
6
7
8
9
|
"SELECT " + "NEW com.codesod.example.jpa.nplusone.dto.PurchaseOrderDTO(P.id, P.orderDate) " + "FROM " + "PurchaseOrder P " + "WHERE " + "P.customerId = :customerId" , PurchaseOrderDTO.class); jpaQuery.setParameter( "customerId" , "Sayem" ); List<PurchaseOrderDTO> orders = jpaQuery.getResultList(); |
Несколько предостережений от использования выражения конструктора —
- Целевой DTO должен иметь конструктор, список параметров которого соответствует выбранным столбцам
- Полное имя класса DTO должно быть указано
Использование объединяющих графиков / диаграмм сущностей
Мы можем использовать JOIN FETCH в наших запросах всякий раз, когда нам нужно извлечь объект со всеми его потомками одновременно. Это приводит к гораздо меньшему трафику базы данных, что приводит к повышению производительности.
Спецификация JPA 2.1 представила Entity Graphs, которая позволяет нам создавать статические / динамические планы загрузки запросов.
Торбен Янссен написал пару постов ( здесь и здесь ), подробно описывающих их использование, которые стоит проверить.
Некоторые примеры кода для этого поста можно найти на Github .
Опубликовано на Java Code Geeks с разрешения Саима Ахмеда, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Советы JPA: Как избежать проблемы выбора N + 1
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |