Вступление
Платформы ORM, такие как JPA, упрощают процесс разработки, помогая нам избежать большого количества стандартного кода во время объектного <-> реляционного отображения данных. Тем не менее, они также приносят некоторые дополнительные проблемы к таблице, и N + 1 является одним из них. В этой статье мы кратко рассмотрим проблему, а также несколько способов ее избежать.
Проблема
В качестве примера я буду использовать упрощенную версию приложения для заказа книг онлайн. В таком приложении я мог бы создать объект, подобный приведенному ниже, для представления Заказа на покупку —
|
01
02
03
04
05
06
07
08
09
10
11
|
@Entitypublic 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
|
@Entitypublic class PurchaseOrderItem { @Id private String id; private String bookId;} |
Эти сущности были значительно упрощены, но для целей этой статьи это подойдет.
Теперь предположим, что нам нужно найти заказы клиента, чтобы отобразить их в истории заказов на покупку. Следующий запрос будет служить этой цели —
|
1
2
3
4
5
6
|
SELECT PFROM PurchaseOrder PWHERE 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 calculationanotherRepository.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 calculationanotherRepository.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, являются их собственными. |