联邦是如何处理N+1查询问题的
学习如何在联邦图中处理返回列表的操作的N+1问题
GraphQL开发者很快就会遇到著名的“N+1 查询问题”,这个问题在返回列表的操作中尤为常见:
query TopReviews {topReviews(first: 10) {idratingproduct {nameimageUrl}}}
在单体GraphQL服务器中,执行引擎采取以下步骤:
- 解析
Query.topReviews
字段,它返回一个Review
列表。 - 对于列表中的每个
Review
,解析Review.product
字段
如果Query.topReviews
字段返回10条评论,则执行器将解析Review.product
字段 10次。如果Reviews.product
字段针对单个Product
执行数据库或REST查询,则会看到对数据源的10个唯一调用。这从以下原因看是不太理想的:
- 一次性查询10个产品更有效率(例如
SELECT * FROM products WHERE id IN (<product ids>)
)。 - 如果有任何评论引用了同一产品,那么我们正在浪费资源去获取已经拥有的数据。
针对单体GraphQLAPI的解决方案是dataloader模式。所有 GraphQL服务器实现都支持这种模式。Apollo服务器文档解释了如何在Node.js服务器中使用JavaScript实现。
联邦图中的N+1问题
考虑相同的TopReviews
操作,但我们已经在单独的子图
中实现了Review
和Product
类型:
幸运的是,查询计划默认处理Product
类型等实体的N+1查询!此操作的查询计划如下:
- 首先,我们使用根字段
Query.topReviews
从Review
子图中Fetch
列表中的Review
。我们还请求每个相关产品的id
。 - 接下来,我们提取
Product
实体 引用并将其批量检索到 Products 子图中的Query._entities
根字段。 - 返回
Product
实体后,我们在Flatten
步骤中将它们合并到Review
列表中。
编写高效的实体解析器
在大多数 subgraph 实现(包括 @apollo/subgraph
)中,我们不直接编写 Query._entities
解析器。相反,我们使用 引用解析器 API 来解析单个实体引用:
const resolvers = {Product: {__resolveReference(productRepresentation) {return fetchProductByID(productRepresentation.id);},},};
此 API 的动机与 subgraph 规范的微妙、关键方面相关:解析实体的顺序必须与给定实体引用的顺序匹配。如果我们以错误的顺序返回实体,则这些 字段 将与错误的实体合并,我们将得到错误的结果。为了避免此类问题,大多数 subgraph 库会为您处理实体顺序。
但这会再次引入 N+1 查询问题:在上面的示例中,我们将为每个实体引用调用一次 fetchProductByID
。
幸运的是,在单体图中也存在相同的解决方案:数据加载器。在几乎所有情况下,引用 解析器 都应该使用数据加载器。
const resolvers = {Product: {__resolveReference(product, context) {return context.dataloaders.products(product.id);},},};
现在,当查询规划器调用带有产品
实体批的产品子图时,我们将对产品数据源发出单个批处理请求。