JPA技巧:避免N + 1选择问题

本文探讨了JPA ORM框架中常见的N+1查询问题,该问题在处理大量数据时会导致性能下降。文章通过举例说明了一个在线图书订购应用如何遇到此问题,并提出了解决方案,包括避免贪婪加载,只获取必要数据,以及使用JOIN FETCH和实体图来提高查询效率。
摘要由CSDN通过智能技术生成

介绍

诸如JPA的ORM框架通过帮助我们在对象<->关系数据映射期间避免了很多样板代码,从而简化了我们的开发过程。 但是,它们还会给表带来一些其他问题,N + 1是其中之一。 在本文中,我们将简短地探讨该问题以及避免这些问题的一些方法。

问题

作为示例,我将使用在线图书订购应用程序的简化版本。 在这样的应用程序中,我可能会创建一个如下所示的实体来代表采购订单–

@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<>();
}

采购订单由订单ID,客户ID和要购买的一个或多个项目组成。 PurchaseOrderItem实体可能具有以下结构–

@Entity
public class PurchaseOrderItem {

    @Id
    private String id;

    private String bookId;
}

这些实体已经简化了很多,但是出于本文的目的,这是可以做到的。

现在假设我们需要找到一个客户的订单以在他们的采购订单历史中显示它们。 以下查询将用于此目的–

SELECT
    P
FROM
    PurchaseOrder P
WHERE
    P.customerId = :customerId

转换为SQL后,其外观如下所示–

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个查询以获取所有订单商品。

当我们的数据增长时,此行为为我们带来了可伸缩性问题。 即使适量的订单和物品也会造成严重的性能问题。

解决方案

避免渴望获取

这是问题背后的主要原因。 我们应该摆脱从映射中获取的所有渴望。 它们几乎没有任何好处可证明其可用于生产级应用程序。 我们应该将所有关系标记为“懒惰”。

需要注意的重要一点–将关系映射标记为“惰性”并不保证基础持久性提供程序也将其同样对待。 JPA规范不保证延迟获取。 充其量对持久性提供程序而言是一个提示。 但是,考虑到Hibernate,我从未见过这样做。

仅获取实际需要的数据

始终建议使用此方法,而不考虑是否要进行急切/懒惰的访存。

我记得我进行过一次N + 1优化,将REST端点的最大响应时间从17分钟提高到1.5秒 。 端点正在根据某些条件获取单个实体,对于我们当前的示例,该实体将遵循以下原则:

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());

id是结果中唯一用于后续计算的数据。

有几个客户有超过一千个订单。 每个命令依次又有数千个其他几种不同类型的子级。 不用说,每当在此端点接收到针对这些订单的请求时,数据库中就会执行数千个查询。
为了提高性能,我所做的就是-

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提供的Constructor表达式–

"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();

使用构造函数表达式的一些注意事项–

  1. 目标DTO必须具有其参数列表与要选择的列匹配的构造函数
  2. 必须指定DTO类的完全限定名称

使用联接提取/实体图

每当我们需要同时获取带有所有子元素的实体时,就可以在查询中使用JOIN FETCH 。 这样可以减少数据库流量,从而提高性能。

JPA 2.1规范引入了实体图,它使我们可以创建静态/动态查询负载计划。
Thorben Janssen这里这里 )写了几篇文章,详细介绍了它们的用法,值得一看。
这篇文章的一些示例代码可以在Github上找到。

翻译自: https://www.javacodegeeks.com/2018/04/jpa-tips-avoiding-the-n-1-select-problem.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值