欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

解决 Hibernate N+1 问题

程序员文章站 2024-03-15 15:44:59
...

问题

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

@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 个查询。

解决方法

避免使用立即抓取(Eager Fetching)

这是问题背后的主要原因。我们应该从我们的映射中摆脱所有立即抓取。它们几乎没有任何好处可以证明它们在生产级应用中的使用。我们应该将所有关系标记为懒惰。

只抓取你真实需要的数据

有时候我们并不想在查询订单时关联出所有的订单记录,我们可以将订单记录设为懒加载,在自己真实需要时再去查询对应的数据。

在 JPQL 中使用 Fetch Join

初始化延迟关联的更好选择是使用带有抓取连接的 JPQL 查询。

Query q = this.em.createQuery("SELECT o FROM Order o JOIN FETCH o.items i WHERE o.id = :id");
q.setParameter("id", orderId);
newOrder = (Order) q.getSingleResult();

这告诉实体管理器在同一查询中加载所选实体和关系。

使用 BatchSize 批量抓取

批量抓取是惰性选择抓取策略的优化。假设该订单的商品条目有 25 个,当配置了 BatchSize 后,在请求订单时,查询将变为 3 条,每条语句使用 In 查询 5 个商品条目。

使用 @BatchSize 注解可以配置到懒加载的集合或对象上。

@Entity
@BatchSize(size=100)
class PurchaseOrderItem {
...
}

@OneToMany
@BatchSize(size = 5) /
private List<PurchaseOrderItem> purchaseOrderItems() = { ... };

实体图(Entity Graph)

实体图是特定化持久性查询或操作的模板。它们在创建**抓取方案(fetch plans)**或同时检索的持久字段组时使用。应用程序开发人员使用抓取方案将相关的持久字段组合在一起以提高运行时性能。

默认情况下,实体字段或属性是**懒抓取(lazy fetch)**的。开发人员将字段或属性指定为抓取方案的一部分,持久性 provider 将立即抓取(eager fetch)它们。

我们可以使用注解部署描述符(比如 web.xml)静态创建实体图,也可以使用标准接口动态创建实体图。

实体图定义了在查找或查询操作期间需要立即抓取的字段。

默认,实体的所有字段都是懒抓取,除非指定了实体元数据的 fetch 属性为 javax.persistence.FetchType.EAGER。始终提取实体类的主键和版本字段,不需要将其显式添加到实体图中。

创建的实体图可以是 fetch graph(抓取图)load graph(加载图)

Fetch Graphs(抓取图)

javax.persistence.fetchgraph 属性用于指定实体图时,实体图的属性节点指定的属性将被视为 FetchType.EAGER,未指定的属性将被视为 FetchType.LAZY。 以下规则适用,具体取决于属性类型。

Load Graphs(加载图)

javax.persistence.loadgraph 属性用于指定实体图时,实体图的属性节点指定的属性将被视为 FetchType.EAGER,未指定的属性将根据其指定的或默认的FetchType 进行处理。

命名实体图(Named Entity Graph)

命名实体图是由应用于实体类的 @NamedEntityGraph 注解定义的实体图,或应用程序部署描述符中的 named-entity-graph 元素。部署描述符中定义的命名实体图覆盖任何具有相同名称的基于注解的实体图。

通过使用 javax.persistence.NamedAttributeNode 注解在 @NamedEntityGraphattributeNodes 元素中指定字段,将字段添加到实体图中:

@NamedEntityGraph(name="emailEntityGraph", attributeNodes={
    @NamedAttributeNode("subject"),
    @NamedAttributeNode("sender")
})
@Entity
public class EmailMessage {
    @Id
    String messageId;
    String subject;
    String body;
    String sender;
}

通过在 @NamedEntityGraphs 注解中对多个 @NamedEntityGraph 定义进行分组,可以将多个 @NamedEntityGraph 定义应用于类。

@NamedEntityGraphs({
    @NamedEntityGraph(name="previewEmailEntityGraph", attributeNodes={
        @NamedAttributeNode("subject"),
        @NamedAttributeNode("sender"),
        @NamedAttributeNode("body")
    }),
    @NamedEntityGraph(name="fullEmailEntityGraph", attributeNodes={
        @NamedAttributeNode("sender"),
        @NamedAttributeNode("subject"),
        @NamedAttributeNode("body"),
        @NamedAttributeNode("attachments")
    })
})
@Entity
public class EmailMessage { ... }

通过为命名实体图调用 EntityManager.getEntityGraph 来获取定义的命名实体图。

EntityGraph<EmailMessage> eg = em.getEntityGraph("emailEntityGraph");

在查询操作中使用 Entity Graphs

要为有类型和无类型查询指定实体图,请在查询对象上调用 setHint 方法,并指定 javax.persistence.loadgraphjavax.persistence.fetchgraph 作为属性名称,并将 EntityGraph 实例指定为值:

EntityGraph<EmailMessage> eg = em.getEntityGraph("previewEmailEntityGraph");
List<EmailMessage> messages = em.createNamedQuery("findAllEmailMessages")
        .setParameter("mailbox", "inbox")
        .setHint("javax.persistence.loadgraph", eg)
        .getResultList();

有类型的查询使用相同的技术:

EntityGraph<EmailMessage> eg = em.getEntityGraph("previewEmailEntityGraph");

CriteriaQuery<EmailMessage> cq = cb.createQuery(EmailMessage.class);
Root<EmailMessage> message = cq.from(EmailMessage.class);
TypedQuery<EmailMessage> q = em.createQuery(cq);
q.setHint("javax.persistence.loadgraph", eg);
List<EmailMessage> messages = q.getResultList();

动态实体图(Dynamic Entity Graph)

创建动态实体图可以使用:EntityManager.createEntityGraph

动态实体图 类似于命名的 entity graph。唯一的区别是,entity graph 是通过 Java API 定义的。

EntityGraph graph = this.em.createEntityGraph(Order.class);
Subgraph itemGraph = graph.addSubgraph("items");
    
Map hints = new HashMap();
hints.put("javax.persistence.loadgraph", graph);
  
Order order = this.em.find(Order.class, orderId, hints);

使用代码动态创建 entity graph 允许我们可以不使用实体上的注解。因此,如果您需要创建一个不会重复使用的特定于用例的图表,我建议使用动态实体图。如果要重用实体图,则更容易注释命名实体图。

Spring Data JPA 中使用 Entity Graph

在 Spring Data JPA 中,我们可以通过在查询接口方法上使用注解 org.springframework.data.jpa.repository.EntityGraph 来定义命名实体图或动态实体图:

  • 通过指定 value 属性指定命名实体图

  • 通过指定 attributePaths 属性动态定义实体图

    该属性为数组类型,我们可以定义多个 attribute,也可以通过 property.nestedProperty 形式来定义实体对象字段嵌套的属性

    @EntityGraph(attributePaths = {"questions", "questions.questionOptions", "questions.answers"})
        Optional<Questionnaire> findOneByProject_Id(Long id);
    

原文链接:解决 Hibernate N+1 问题