函数指针使用场景和选择
N + 1问题是使用ORM解决方案时的常见问题。 当您将某些@OneToMany关系的fetchType设置为lazy时,就会发生这种情况,以便仅在访问Set / List时才加载子实体。 假设我们有一个具有两个关系的Customer实体:每个客户的一组订单和一组地址。
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<OrderEntity> orders;@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<AddressEntity> addresses;
要加载所有客户,我们可以发出以下JPQL语句,然后加载每个客户的所有订单:
List<CustomerEntity> resultList = entityManager.createQuery("SELECT c FROM CustomerEntity AS c", CustomerEntity.class).getResultList();
for(CustomerEntity customerEntity : resultList) {Set<OrderEntity> orders = customerEntity.getOrders();for(OrderEntity orderEntity : orders) {...}
}
Hibernate 4.3.5(与JBoss AS Wildfly 8.1.0CR2一起提供)将从数据库中仅为两个(!)客户生成以下一系列SQL语句:
Hibernate: selectcustomeren0_.id as id1_1_,customeren0_.name as name2_1_,customeren0_.numberOfPurchases as numberOf3_1_ fromCustomerEntity customeren0_
Hibernate: selectorders0_.CUSTOMER_ID as CUSTOMER4_1_0_,orders0_.id as id1_2_0_,orders0_.id as id1_2_1_,orders0_.campaignId as campaign2_2_1_,orders0_.CUSTOMER_ID as CUSTOMER4_2_1_,orders0_.timestamp as timestam3_2_1_ fromOrderEntity orders0_ whereorders0_.CUSTOMER_ID=?
Hibernate: selectorders0_.CUSTOMER_ID as CUSTOMER4_1_0_,orders0_.id as id1_2_0_,orders0_.id as id1_2_1_,orders0_.campaignId as campaign2_2_1_,orders0_.CUSTOMER_ID as CUSTOMER4_2_1_,orders0_.timestamp as timestam3_2_1_ fromOrderEntity orders0_ whereorders0_.CUSTOMER_ID=?
如我们所见,第一个查询从表CustomerEntity中选择所有客户。 接下来的两个选择先提取,然后在第一个查询中加载我们已加载的每个客户的订单。 当我们有100个客户而不是2个客户时,我们将获得101个查询。 一个初始查询可加载所有客户,然后针对100个客户中的每个客户,另外查询一个订单。 这就是为什么将此问题称为N + 1的原因。
解决此问题的常见习惯是强制ORM生成内部联接查询。 在JPQL中,这可以通过使用JOIN FETCH子句来完成,如以下代码片段所示:
entityManager.createQuery("SELECT c FROM CustomerEntity AS c JOIN FETCH c.orders AS o", CustomerEntity.class).getResultList();
正如预期的那样,ORM现在使用OrderEntity表生成一个内部联接,因此只需要一个SQL语句即可加载所有数据:
selectcustomeren0_.id as id1_0_0_,orders1_.id as id1_1_1_,customeren0_.name as name2_0_0_,orders1_.campaignId as campaign2_1_1_,orders1_.CUSTOMER_ID as CUSTOMER4_1_1_,orders1_.timestamp as timestam3_1_1_,orders1_.CUSTOMER_ID as CUSTOMER4_0_0__,orders1_.id as id1_1_0__
fromCustomerEntity customeren0_
inner joinOrderEntity orders1_on customeren0_.id=orders1_.CUSTOMER_ID
在您知道必须为每个客户加载所有订单的情况下,JOIN FETCH子句将SQL语句的数量从N + 1减少到1。这当然具有缺点,即您现在要转移一个订单的所有订单。客户一次又一次的客户数据(由于查询中的其他客户列)。
JPA规范从版本2.1引入,即所谓的NamedEntityGraphs。 通过此批注,您可以描述JPQL查询应加载的图形,而不是JOIN FETCH子句可以执行的图形,从而为N + 1问题提供了另一种解决方案。 下面的示例演示了我们的客户实体的NamedEntityGraph,该实体应该仅加载客户名称及其订单。 订单在子图ordersGraph中有更详细的描述。 在这里,我们看到我们只想加载订单的字段ID和CampaignId。
@NamedEntityGraph(name = "CustomersWithOrderId",attributeNodes = {@NamedAttributeNode(value = "name"),@NamedAttributeNode(value = "orders", subgraph = "ordersGraph")},subgraphs = {@NamedSubgraph(name = "ordersGraph",attributeNodes = {@NamedAttributeNode(value = "id"),@NamedAttributeNode(value = "campaignId")})}
)
在通过NameManager通过EntityManager加载JPQL查询后,将其命名为JPQL查询的提示:
EntityGraph entityGraph = entityManager.getEntityGraph("CustomersWithOrderId");
entityManager.createQuery("SELECT c FROM CustomerEntity AS c", CustomerEntity.class).setHint("javax.persistence.fetchgraph", entityGraph).getResultList();
Hibernate从版本4.3.0.CR1开始支持@NamedEntityGraph批注,并为上面显示的JPQL查询创建以下SQL语句:
Hibernate: selectcustomeren0_.id as id1_1_0_,orders1_.id as id1_2_1_,customeren0_.name as name2_1_0_,customeren0_.numberOfPurchases as numberOf3_1_0_,orders1_.campaignId as campaign2_2_1_,orders1_.CUSTOMER_ID as CUSTOMER4_2_1_,orders1_.timestamp as timestam3_2_1_,orders1_.CUSTOMER_ID as CUSTOMER4_1_0__,orders1_.id as id1_2_0__ fromCustomerEntity customeren0_ left outer joinOrderEntity orders1_ on customeren0_.id=orders1_.CUSTOMER_ID
我们看到Hibernate不会发出N + 1查询,而是@NamedEntityGraph注释强制Hibernate为每个左外部联接加载订单。 当然,这与FETCH JOIN子句有微妙的区别,在子句中,Hibernate创建了内部联接。 与FETCH JOIN子句相反,左外部联接还将加载不存在订单的客户,在FETCH JOIN子句中,我们仅加载具有至少一个订单的客户。
有趣的是,Hibernate加载的负载比表CustomerEntity和OrderEntity的指定属性更多。 由于这与@NamedEntityGraph的规范(第3.7.4节)相冲突,因此我为此创建了一个JIRA问题 。
结论
我们已经看到,在JPA 2.1中,我们为N + 1问题提供了两种解决方案:我们可以使用FETCH JOIN子句来急切地获取一个@OneToMany关系,这将导致一个内部联接,或者我们可以使用@NamedEntityGraph功能使我们通过左外部联接指定要加载的@OneToMany关系。
翻译自: https://www.javacodegeeks.com/2014/07/using-namedentitygraph-to-load-jpa-entities-more-selectively-in-n1-scenarios.html
函数指针使用场景和选择