学习执行

执行

了解 GraphQL 如何为请求的字段提供数据

在解析的文档被验证之后,客户端的请求将由 GraphQL 服务器执行,返回的结果将反映请求的查询的结构。在本页中,您将了解 GraphQL 操作的执行阶段,在该阶段中,数据会根据客户端请求了哪些字段而从现有源中读取或写入现有源。

字段解析器

如果没有类型系统,GraphQL 无法执行操作,因此让我们使用一个示例类型系统来说明查询的执行过程。这是本指南中所有示例中使用的相同类型系统的一部分

type Query {
  human(id: ID!): Human
}
 
type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}
 
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}
 
type Starship {
  name: String
}

为了描述执行查询时发生的情况,让我们通过一个示例来了解一下

操作
响应

您可以将 GraphQL 查询中的每个字段视为前一个类型的函数或方法,它返回下一个类型。GraphQL 的工作方式正是如此——每个类型上的每个字段都由 GraphQL 服务器开发人员编写的解析器函数支持。当执行一个字段时,会调用相应的解析器来生成下一个值。

如果一个字段生成一个标量值(如字符串或数字),则执行完成。但是,如果一个字段生成一个对象值,那么查询将包含应用于该对象的另一个字段选择。这会一直持续到到达叶子值。GraphQL 查询总是以标量类型或枚举类型结束。

根字段和解析器

在每个 GraphQL 服务器的顶层是一个对象类型,它表示进入 GraphQL API 的可能入口点,通常称为“query 根操作类型”或 Query 类型。如果 API 还支持用于写入数据的 mutation 和用于获取实时数据的 subscription,那么它还将具有 MutationSubscription 类型,这些类型会暴露用于执行此类操作的字段。您可以在Schema and Types 页面上了解有关这些类型的更多信息。

在这个示例中,我们的 Query 类型提供了一个名为 human 的字段,它接受参数 id。该字段的解析器函数可能会访问数据库,然后构造并返回一个 Human 类型。

function resolveHumanQuery(obj, args, context, info) {
  return context.db.loadHumanByID(args.id).then(userData => new Human(userData));
}

此示例使用 JavaScript 编写,但 GraphQL 服务器可以用多种不同的语言构建。在参考实现中,解析器函数接收四个参数

  • obj:上一个对象(对于根 Query 类型的字段,此参数通常不使用)。
  • args:在 GraphQL 操作中提供给字段的参数。
  • context:提供给每个解析器的值,可能包含重要上下文信息,例如当前登录的用户或对数据库的访问。
  • info:通常只在高级用例中使用,这是一个包含与当前操作相关的特定于字段的信息以及模式详细信息的值;有关详细信息,请参阅 type GraphQLResolveInfo

请注意,虽然查询操作在执行期间可能在技术上向底层数据系统写入数据,但 mutation 操作通常用于在执行期间产生副作用的请求。由于 mutation 操作会产生副作用,因此可以期望 GraphQL 实现将这些字段序列化执行。

异步解析器

让我们仔细看看此解析器函数中发生了什么

function resolveHuman(obj, args, context, info) {
  return context.db.loadHumanByID(args.id).then(userData => new Human(userData));
}

GraphQL 查询中的 id 参数指定请求数据的用户,而 context 提供访问权限以从数据库检索此数据。由于从数据库加载是异步操作,因此这会返回一个Promise。在 JavaScript 中,Promise 用于处理异步值,但相同概念存在于许多语言中,通常称为FutureTaskDeferred。当数据库返回数据时,我们可以构造并返回一个新的 Human 对象。

请注意,虽然解析器函数需要知道 Promise,但 GraphQL 查询不需要。它只是期望 human 字段返回可以进一步解析为标量 name 值的内容。在执行期间,GraphQL 将等待 Promise、Future 和 Task 完成后再继续,并将以最佳并发方式进行。

简单解析器

现在 Human 对象可用,GraphQL 执行可以继续处理为此类型请求的字段

function resolveHumanName(obj, args, context, info) {
  return obj.name;
}

GraphQL 服务器由类型系统驱动,该系统用于确定下一步要做什么。甚至在 human 字段返回任何内容之前,GraphQL 就知道下一步将是解析 Human 类型上的字段,因为类型系统告诉它 human 字段将返回此输出类型。

在这种情况下解析 name 很简单。调用 name 解析器函数,obj 参数是从上一个字段返回的新 Human 对象。在这种情况下,我们期望此对象具有 name 属性,我们可以直接读取并返回该属性。

许多 GraphQL 库允许您省略如此简单的解析器,假设如果未为字段提供解析器,则应读取并返回同名属性。

标量类型转换

在解析 name 字段时,appearsInstarships 字段可以并发解析。appearsIn 字段也可以有一个简单的解析器,但让我们仔细看看

const Human = {
  appearsIn(obj) {
    return obj.appearsIn; // e.g. [4, 5, 6]
  },
};

请注意,我们的类型系统声称 appearsIn 将返回具有已知值的 Enum 类型,然而,此函数返回的是数字!确实,如果我们查看结果,我们会看到 Enum 类型的适当值正在返回。发生了什么事?

这是标量类型转换的一个示例。类型系统知道期望什么,并将解析器函数返回的值转换为能维持 API 合约的内容。在这种情况下,我们的服务器上可能定义了一个 Enum 类型,它在内部使用数字,如 456,但在 GraphQL 类型系统中将其表示为期望的值。

列表解析器

我们已经通过上面的 appearsIn 字段看到了一些关于字段返回事物列表时会发生什么。它返回了一个包含 Enum 类型值的列表类型,由于类型系统期望的就是这样,列表中的每个项都被转换成了适当的值。当解析 starships 字段时会发生什么?

function resolveHumanStarships(obj, args, context, info) {
  return Promise.all(
    obj.starshipIDs.map(id =>
      context.db.loadStarshipByID(id).then(shipData => new Starship(shipData))
    )
  );
}

此字段的解析器不仅仅返回一个 Promise,它返回的是一个 Promise 列表Human 对象有一个他们驾驶的 Starships 的 ID 列表,但我们需要加载所有这些 ID 才能获得真实的 Starship 对象。

GraphQL 将并发等待所有这些 Promise 完成后才继续,当留下一个对象列表时,它将再次继续并发地加载每个对象的 name 字段。

生成结果

当每个字段被解析时,结果值被放入一个键值映射中,键是字段名称(或别名),值是被解析的值。这从查询的底部叶子字段一直向上到根 Query 类型的原始字段持续进行。这些共同生成一个反映原始查询的结构,然后可以将其(通常作为 JSON)发送给请求它的客户端。

让我们最后再看一下原始查询,看看所有这些解析函数是如何生成结果的

操作
响应

正如我们所见,在查询执行期间,嵌套选择集中的每个字段都解析为一个标量叶子值。

后续步骤

总结一下我们学到的关于执行的内容

  • GraphQL 类型系统中的每个字段都将有一个相应的解析器函数,该函数从现有数据源为该字段提供数据
  • 执行从顶层 QueryMutationSubscription 字段开始
  • 解析器可以异步执行
  • 标量类型转换将值转换为模式所期望的类型
  • 当对象类型的字段返回其他对象的列表类型时,可能需要从底层数据源获取额外数据,以将任何外部键状引用(如 ID)转换为相关的对象
  • 一旦所有请求的字段都解析为期望的叶子值,结果就会发送给客户端,通常是 JSON 格式

既然我们了解了操作是如何执行的,我们就可以进入 GraphQL 请求生命周期的最后阶段,即响应被交付给客户端。

下一课

响应

探索 GraphQL 如何构建其响应,包括数据、错误和用于自定义元数据的扩展。

前往下一课 教程