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

查询计划

了解你的路由器如何协调子图间的操作


了解 以帮助你调试 Apollo Federation 的高级用法。

当你的 接收到一个进入的 时,它需要确定如何使用你的 来填充数据为每个操作的 。要做到这一点,路由器生成一个 查询计划

Parallel
Fetch (reviews)
Fetch (users)
Fetch (hotels)
Flatten (latestReviews,[],hotel)

一个 是将单个进入的操作分解为多个可由单个 解决的操作的蓝图。其中一些操作依赖于其他操作的结果,因此查询计划还定义了它们执行所需的任何排序。

示例图

假设我们的联邦 包含以下这些 子图:

酒店子图
type Hotel @key(fields: "id") {
id: ID!
address: String!
}
type Query {
hotels: [Hotel!]!
}
评论子图
type Hotel @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
type Review {
id: ID!
rating: Int!
description: String!
}

基于这些子图,客户可以对我们的路由器执行以下

query GetHotels {
hotels { # Resolved by Hotels subgraph
id
address
reviews { # Resolved by Reviews subgraph
rating
}
}
}

查询包括来自酒店子图评论子图的字段。因此,路由器需要向每个子图发送至少一个查询,以填充所有请求的字段。

查看此查询路由器查询计划

这种语法可能看起来很复杂。🤔 让我们来拆解它。

查询计划的构架

一个查询计划被定义为一组节点的层次结构,当序列化成JSON或GraphQL时看起来像。

每个查询计划的最高层是这样的QueryPlan节点:

QueryPlan {
...
}

QueryPlan节点内定义的每个节点是以下之一:

节点描述
Fetch通知路由器在某个子图上执行特定操作。
Parallel通知路由器节点的直接子节点可以并行执行。
Sequence通知路由器节点直接的子节点必须按照列表中的顺序依次执行。
Flatten通知路由器将节点子节点的数据与当前Sequence中先前返回的数据合并。
Defer通知路由器在同一级别的嵌套中的一个或多个@defered字段块。该节点包含一个主块和一个延迟块数组。
Skip/Include通知路由器将查询计划分成两个可以在运行时改变的可能的路径。

以下对每个选项做了更详细的描述。

Fetch节点

一个Fetch节点告诉路由器在特定的子图上执行特定的GraphQL操作。每个查询计划至少包含一个Fetch节点。

# Executes the query shown on the "books" subgraph
Fetch(service: "books") {
{
books {
title
author
}
}
},

节点的主体是要执行的操作,其service参数要对哪个子图执行操作。

在我们的示例图中,以下查询只从Hotels子图中获取数据:

query GetHotels {
hotels {
id
address
}
}

因为这个操作不需要在多个子图之间协调操作,所以整个查询计划中只包含一个Fetch节点:

QueryPlan {
Fetch(service: "hotels") {
{
hotels {
id
address
}
}
},
}

Fetch节点在跨子图解析引用时,它会使用特殊的语法。有关详情,请参阅使用Flatten解析引用

Parallel 节点

一个Parallel节点告诉路由器该节点的所有直接子节点可以并行执行。此节点出现在查询计划中,当路由器可以在不同的子图上执行完全独立的操作时。

Parallel {
Fetch(...) {
...
},
Fetch(...) {
...
},
...
}

例如,假设我们的联邦有一个Books子图和一个Movies 子图。假设客户端执行以下查询来获取书籍和电影的两个单独列表:

query GetBooksAndMovies {
books {
id
title
}
movies {
id
title
}
}

在这种情况下,每个子图返回的数据不依赖于其他子图返回的数据。因此,路由器可以并行查询这两个子图。

操作的查询计划看起来是这样:

Sequence 节点

一个Sequence节点告诉路由器节点的前驱子节点必须按列表顺序依次执行。

Sequence {
Fetch(...) {
...
},
Flatten(...) {
Fetch(...) {
...
}
},
...
}

该节点出现在查询计划中,当一个子图的响应依赖于另一个子图首先返回的数据时。这种情况最常见于查询请求跨多个子图定义的实体的字段时。

以一个例子来说,我们可以回到我们GetHotels查询,它来自我们的示例图

query GetHotels {
hotels { # Resolved by Hotels subgraph
id
address
reviews { # Resolved by Reviews subgraph
rating
}
}
}

在我们的示例中,Hotel类型是一个实体。Hotel.idHotel.address由Hotels子图解析,但Hotel.reviews由Reviews子图解析。我们的Hotels子图需要首先解析,因为否则Reviews子图不知道为哪些酒店返回评论。

操作的查询计划看起来是这样:

如图所示,此查询计划定义了一个Sequence,在执行Reviews子图的Fetch操作之前,首先在Hotels子图上执行。(我们将在下文中介绍Flatten节点和第二个Fetch操作的特殊语法。)

Flatten 节点

一个Flatten节点始终出现在一个Sequence节点内部,并且它始终包含一个Fetch节点。它告诉路由器将其Fetch节点返回的数据与之前在当前Sequence期间Fetch获取的数据合并:

Flatten(path: "hotels.@") {
Fetch(service: "reviews") {
...
}
}

Flatten节点的path参数告诉路由器在哪里将新返回的数据与现有数据合并。@元素在path中表示前面的路径元素返回一个列表。

在上面的示例中,Flatten's Fetch返回的数据被添加到Sequence's 存在于hotels列表

扩展示例

再次回到我们的GetHotels查询的示例图

query GetHotels {
hotels { # Resolved by Hotels subgraph
id
address
reviews { # Resolved by Reviews subgraph
rating
}
}
}

此操作的查询计划首先指示路由器在Hotels子图上执行此查询:

{
hotels {
id
address
__typename # The router requests this to resolve references (see below)
}
}

到目前为止,我们还需要每个酒店的评论相关信息。查询计划接下来指示路由器查询Reviews子图,列出具有以下结构的Hotel对象:

{
reviews {
rating
}
}

现在,路由器需要知道如何将这些Hotel对象与它已经从Hotels子图中获取的数据合并。Flatten节点的path参数刚好告诉它这一点:

Flatten(path: "hotels.@") {
...
}

换句话说,“将Reviews子图返回的Hotel对象与顶层hotels字段返回的第一个查询中的Hotel对象合并。”

当路由器完成此合并时,结果数据与客户端原始查询的结构完全一致:

{
hotels {
id
address
reviews {
rating
}
}
}

使用 Flatten 解析引用

与序列节点类似,Flatten 节点在任何子图响应依赖于必须由另一个子图先返回的数据时出现。这几乎总是涉及解决定义在多个子图中的实体字段。

在这些情况下,Flatten 节点的 Fetch 需要在检索实体的字段之前解决对实体的引用。在这种情况下,Fetch 节点使用特殊语法:

Flatten(path: "hotels.@") {
Fetch(service: "reviews") {
{
... on Hotel {
_typename
id
}
} =>
{
... on Hotel {
reviews {
rating
}
}
}
},
}

与包含 GraphQL 操作不同,这个 Fetch 节点包含两个 GraphQL ,由 => 分隔。

  • 第一个 是正在解决的实体(在这种情况下,Hotel)的表示。了解更多关于实体表示的信息
  • 第二个片段包含路由器需要子图解决(在这种情况下,Hotel.reviewsReview.rating)的实体字段和子字段。

当路由器看到这个特殊的 Fetch 语法时,它知道要查询子图的 Query._entities 字段。此字段使得子图能够直接访问实体的任何可用字段。

现在你已经了解了每个查询计划节点,请再次查看 示例图中的示例查询计划,以了解这些节点如何在一个完整的查询计划中一起工作。

延迟节点

一个 Defer 节点对应于查询计划中同一嵌套层级的多个 @defer

此节点包含一个 主块和一系列 延迟块。主块表示不延迟的查询部分。每个延迟块对应于查询的一个延迟部分。

阅读更多关于如何在 @defer 的路由器支持文章

QueryPlan {
Defer {
Primary {
Fetch(...) {}
}, [
Deferred(...) {
Flatten(...) {
Fetch(...) {}
}
}
]
}
}

条件节点

一个 SkipInclude 节点将查询计划分为 if-else 分支。当操作包含 @skip@include 指令时,会使用条件节点,以便查询计划可以根据提供的运行时 选择不同的节点。

QueryPlan {
Sequence {
Fetch(...) {}
Include(...) {
Flatten(...) {
Fetch(...) { }
}
}
}
}
QueryPlan {
Sequence {
Fetch(...) {}
Skip(...) {
Flatten(...) {
Fetch(...) { }
}
}
}
}

查看查询计划

您可以通过以下任何方式查看特定操作的查询计划:

以标题输出查询计划

自从Apollo Router Corev0.16.0+ 和 @apollo/gatewayv2.5.4+起,您可以在GraphQL响应中返回以下头信息来包含查询计划:

使用@apollo/gateway输出查询计划

您的网关可以在计算过程中输出每个传入的操作的查询计划。要这样做,请将以下内容添加到初始化您的ApolloGateway实例的文件中:

  1. @apollo/query-planner库导入serializeQueryPlan函数:

    const {serializeQueryPlan} = require('@apollo/query-planner');
  2. experimental_didResolveQueryPlan选项添加到您传递给ApolloGateway构造函数的对象中:

    const gateway = new ApolloGateway({
    experimental_didResolveQueryPlan: function(options) {
    if (options.requestContext.operationName !== 'IntrospectionQuery') {
    console.log(serializeQueryPlan(options.queryPlan));
    }
    }
    });

上一页
监视
下一页
错误代码
评分文章评分

©2024Apollo Graph Inc.,从事Apollo GraphQL业务。

隐私政策

公司