加入我们,于10月8日至10日在纽约市学习关于GraphQL联邦和API平台工程的最新的技巧、趋势和新闻。加入我们,参加2024年纽约市的GraphQL峰会
文档
免费开始

处理 N+1 问题

使用 dataloader 模式增强子图性能


开发者经常遇到返回列表的操作中的 "N+1 问题"。考虑以下TopReviews 查询:

query TopReviews {
topReviews(first: 10) {
id
rating
product {
name
imageUrl
}
}
}

在一个单体 中,执行引擎会采取以下步骤:

  1. 解析 Query.topReviews ,它返回一组 Review
  2. 对于每个 Review,解析 Review.product 字段

如果 Query.topReviews 返回十个评论,则执行引擎会解析 Review.product 字段 十次。如果 Reviews.product 字段 对单个 Product 进行数据库或 REST 查询,则对数据源的调用有十次是独立的。这是不理想的,原因如下:

  • 使用单次查询获取所有产品更高效 —— 举例来说,SELECT * FROM products WHERE id IN (<product ids>)
  • 如果任何评论引用了相同的产品,则获取已检索数据的资源会被浪费。

联邦图中的N+1问题

考虑相同的 TopReviews ,它以Reviews和Products子图中定义的Product类型作为

评论子图
type Query {
topReviews(first: Int): [Review]
}
type Review {
id: ID
rating: Int
product: Product
}
type Product @key(fields: "id") {
id: ID
}
产品子图
type Product @key(fields: "id") {
id: ID!
name: String
imageUrl: String
}

大多数 实现使用引用解析器返回对应键的实体对象。虽然这种模式很简单,但当客户端操作请求许多实体的 时,可能会降低性能。回顾一下topReviews 查询,现在在联邦的上下文中:

query TopReviews {
topReviews(first: 10) { # Defined in Reviews subgraph
id
rating
product { # ⚠️ NOT defined in Reviews subgraph
name
imageUrl
}
}
}

路由器执行两个查询:

  1. 从评论子图中获取除Product.nameProduct.imageURL之外的所有 字段
  2. 从产品子图中获取每个产品的name和imageURL。

在产品子图中,Product的引用解析器不接受键的列表,而是一个单一的键。因此,子图库为每个键调用一次引用解析器:

resolvers.js
// Products subgraph
const resolvers = {
Product: {
__resolveReference(productRepresentation) {
return fetchProductByID(productRepresentation.id);
}
},
// ...other resolvers...
}

一个简单的fetchProductByID函数在每次调用时可能会进行数据库调用。如果您需要解决Product.name对于N个不同的产品,这将导致N次数据库调用。这些调用是在除了Review子图进行获取初始评论列表和每个产品的id的调用之外进行的。这个问题可能会导致性能问题,甚至可能使拒绝服务攻击成为可能。

查询规划以处理N+1查询

默认情况下,router"处理类似Product类型实体的N+1查询。的处理TopReviews操作如下:

  1. 首先,router使用根字段Query.topReviews从Reviews子图中获取Review列表。
  2. 接下来,router提取Product实体引用并将它们批量提取到Products子图Query._entities根字段。
  3. router获取到Product实体后,它通过Flatten步聚将它们合并到Review列表中。

Fetch (reviews)
Fetch (products)
Flatten (topReviews,[],products)

大多数subgraph实现(包括@apollo/subgraph)不会直接编写Query._entities解析器。相反,它们使用引用解析器API来解析单个实体引用:

const resolvers = {
Product: {
__resolveReference(productRepresentation) {
return fetchProductByID(productRepresentation.id);
},
},
};

这个API的动机与子图规范的一个微妙而关键方面有关子图规范:解析的实体的顺序必须与给定的实体引用的顺序相匹配。如果实体以错误的顺序返回,则这些字段将与错误的实体合并,导致结果不正确。为了避免这个问题,大多数子图库都会为您处理实体顺序。

因为顺序很重要,它重新引入了N+1查询问题:在上面的例子中fetchProductByID为每个实体引用调用了一次。

dataloader模式解决方案

解决N+1问题的解决方案——无论是联邦图还是——是dataloader模式。例如,在一个实现中,使用dataloader可能看起来像这样:

const resolvers = {
Product: {
__resolveReference(product, context) {
return context.dataloaders.products(product.id);
},
},
};

使用dataloader,当查询计划器调用带有一批Product实体的Products子图时,路由器将向Products数据源发送单个分批请求。

几乎每个GraphQL服务器库都提供了dataloader实现,Apollo建议在每个resolver中使用它,即使是那些不为实体或不需要返回列表的。

下一章
简介
评价这篇文章评价在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,又名Apollo GraphQL。

隐私政策

公司