查询 (Queries)

了解如何从 GraphQL 服务器获取数据

GraphQL 支持三种主要的操作类型——查询 (queries)、变更 (mutations) 和订阅 (subscriptions)。在本指南中,我们已经看过几个基础查询的示例,在本页中,你将详细学习如何使用查询操作的各种功能来从服务器读取数据。

字段 (Fields)

简单来说,GraphQL 就是关于请求对象上的特定字段。让我们从查看 Schema 中 Query 类型上定义的 hero 字段开始

type Query {
  hero: Character
}

我们可以看看查询它时得到了什么结果

操作
响应

在创建 GraphQL 文档时,我们总是从一个根操作类型(本例中为 Query 对象类型)开始,因为它是 API 的入口点。从那里开始,我们必须指定我们感兴趣的字段的选择集 (selection set),一直到它们的叶子值,即标量 (Scalar) 或枚举 (Enum) 类型。字段 name 返回一个 String 类型,在本例中是星球大战主要英雄的名字:"R2-D2"

GraphQL 规范指出,请求的结果将返回在响应顶层的 data 键中。如果请求引发了任何错误,则会在顶层的 errors 键中包含出错的信息。从结果中你可以看到,它的形状与查询相同。这对于 GraphQL 至关重要,因为你总是能得到你预期的结果,并且服务器确切地知道客户端在请求哪些字段。

在之前的例子中,我们只是请求了英雄的名字,它返回了一个 String,但字段也可以返回对象类型(及其列表)。在这种情况下,你可以对该对象类型进行子选择 (sub-selection)

操作
响应

GraphQL 查询可以遍历相关对象及其字段,让客户端在一次请求中获取大量相关数据,而不必像传统的 REST 架构那样进行多次往返请求。

请注意,在这个例子中,friends 字段返回一个项的数组。GraphQL 查询对于单个项或项列表看起来是一样的;但是,我们可以根据 Schema 中的指示知道预期的是哪一种。

参数 (Arguments)

如果唯一能做的就是遍历对象及其字段,GraphQL 就已经是一种非常有用的数据获取语言了。但是当你增加了向字段传递参数的能力时,事情就变得更有趣了

type Query {
  human(id: ID!): Human
}

客户端随后必须在查询中提供所需的 id

操作
响应

在像 REST 这样的系统中,你只能传递一组参数——即请求中的查询参数和 URL 片段。但在 GraphQL 中,每个字段和嵌套对象都可以拥有自己的一组参数,这使得 GraphQL 能够完全替代多次 API 获取。

你甚至可以向输出标量类型的字段传递参数;这种做法的一个用例是在服务器端实现一次数据转换,而不是在每个客户端上分别实现

操作
响应

参数可以是许多不同的类型。在上面的例子中,我们使用了一个枚举类型,它代表有限选项集中的一个(在这种情况下是长度单位,METERFOOT)。GraphQL 附带了一组默认类型,但 GraphQL 服务器也可以声明自定义类型,只要它们可以被序列化为你的传输格式即可。

在这里阅读更多关于 GraphQL 类型系统的内容。

操作类型和名称

在上面的例子中,我们一直使用简写语法,即省略操作选择集之前的 query 关键字。除了显式指定操作类型外,我们还可以添加一个唯一的操作名称,这在生产应用中非常有用,因为它使调试和追踪变得更容易。

这是一个包含 query 关键字作为操作类型以及 HeroNameAndFriends 作为操作名称的示例

操作
响应

操作类型可以是 querymutationsubscription,它描述了你打算执行的操作类型。除非你使用查询的简写语法,否则这个关键字是必需的(对于变更和订阅,它总是必需的)。此外,如果你希望为操作提供名称,则也必须指定操作类型。

操作名称是你为操作分配的显式名称;你应该选择一个有意义的名称。当在一个文档中发送多个操作时它是必需的,但即使你只发送一个操作,也鼓励使用它,因为操作名称有助于调试和服务器端日志记录。当出现问题时(你在网络日志或 GraphQL 服务器日志中看到错误),通过名称在代码库中识别查询要比尝试破译其内容容易得多。

可以把它想象成你喜欢的编程语言中的函数名。例如,在 JavaScript 中,我们可以只使用匿名函数,但当我们给函数命名时,更容易追踪它、调试代码并在它被调用时进行记录。同样地,GraphQL 的查询和变更名称,连同片段名称,可以作为服务器端识别不同 GraphQL 请求的有用调试工具。

别名 (Aliases)

如果你目光敏锐,你可能已经注意到,由于结果对象字段与查询中的字段名称匹配但不包含参数,你不能直接用不同的参数查询同一个字段。这就是为什么你需要别名——它们允许你将字段的结果重命名为你想要的任何名称。

操作
响应

在上面的例子中,两个 hero 字段本来会发生冲突,但由于我们可以将它们起别名为不同的名称,我们可以在一次请求中获得两个结果。

变量 (Variables)

到目前为止,我们一直是在查询字符串内部编写所有参数。但在大多数应用中,字段的参数将是动态的。例如,可能有一个下拉列表让你选择感兴趣的星球大战章节,或者一个搜索框,或者一组过滤器。

直接在查询字符串中传递这些动态参数并不是一个好主意,因为这样我们的客户端代码就需要在运行时动态操作查询字符串,并将其序列化为 GraphQL 特定的格式。相反,GraphQL 有一种一等公民的方式将动态值从查询中提取出来,并作为单独的字典传递。这些值被称为变量

当我们开始使用变量时,我们需要做三件事

  1. 将查询中的静态值替换为 $variableName
  2. $variableName 声明为查询接受的变量之一
  3. 在单独的、特定于传输(通常是 JSON)的变量字典中传递 variableName: value

这就是它们结合在一起的样子

操作
变量 (Variables)
响应

要在 GraphQL 文档中使用变量,必须指定操作类型和名称。

现在,在我们的客户端代码中,我们可以简单地传递不同的变量,而不需要构造一个全新的查询。通常,这也是指示查询中哪些参数预期是动态的一种良好实践——我们永远不应该通过字符串插值来从用户提供的值构造查询。

变量定义

变量定义是上述查询中看起来像 ($episode: Episode) 的部分。它的工作方式就像类型化语言中的函数参数定义。它列出了所有以 $ 为前缀的变量,后跟它们的类型,在本例中是 Episode

所有声明的变量必须是标量、枚举或输入对象 (Input Object) 类型。因此,如果你想向字段传递一个复杂对象,你需要知道服务器上与之匹配的输入类型是什么。

变量定义可以是可选的或必需的。在上面的例子中,由于 Episode 类型旁边没有 !,它是可选的。但如果你将变量传递到的字段需要一个非空 (non-null) 参数,那么该变量也必须是必需的。

要了解有关这些变量定义语法的更多信息,学习 Schema 定义语言 (SDL) 很有帮助,这在《Schema 和类型》页面有详细解释。

默认变量

也可以通过在类型声明后添加默认值,为查询中的变量分配默认值

query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

当为所有变量都提供了默认值时,你可以不传递任何变量来调用该查询。如果变量字典中传递了任何变量,它们将覆盖默认值。

片段 (Fragments)

假设我们的应用中有一个相对复杂的页面,它可以让我们并排查看两个英雄及其朋友。你可以想象,这样的查询很快就会变得复杂,因为我们需要重复至少一次字段——对比的每一侧都要重复一次。

这就是为什么 GraphQL 包含了名为片段的可重用单元。片段允许你构建一组字段,然后在需要的查询中包含它们。这是一个如何使用片段解决上述情况的示例

操作
响应

你可以看到,如果我们不能使用片段,上述查询将非常重复。片段的概念经常被用来将复杂的应用数据需求拆分成更小的块,特别是当你需要将许多带有不同片段的 UI 组件组合成一个初始数据获取请求时。

在片段中使用变量

片段也可以访问操作中声明的变量

操作
变量 (Variables)
响应

内联片段 (Inline Fragments)

与许多其他类型系统一样,GraphQL Schema 能够定义接口 (Interface) 和联合 (Union) 类型。你可以在 Schema 和类型页面上了解更多相关信息。

如果你查询的字段返回接口或联合类型,你将需要使用内联片段来访问底层具体类型的数据。通过示例最容易看清

操作
变量 (Variables)
响应

在此查询中,hero 字段返回 Character 类型,根据 episode 参数,它可能是 HumanDroid。在直接选择中,你只能请求 Character 接口上的字段,例如 name

要请求具体类型上的字段,你需要使用带有类型条件 (type condition) 的内联片段。因为第一个片段被标记为 ... on Droid,所以只有当从 hero 返回的 CharacterDroid 类型时,primaryFunction 字段才会执行。对于 Human 类型上的 height 字段也是同理。

具名片段 (Named fragments) 也可以以同样的方式使用,因为具名片段总是附加了类型。

元字段 (Meta fields)

正如我们在联合类型中看到的,在某些情况下你不知道从 GraphQL 服务中会得到什么类型,因此你需要某种方式来确定如何在客户端处理该数据。

GraphQL 允许你在查询的任何点请求元字段 __typename,以获取该点对象类型的名称

操作
响应

在上述查询中,search 返回一个联合类型,它可以是三个选项之一。如果没有 __typename 字段,客户端将无法区分不同的类型。

所有以双下划线 (__) 开头的字段名称都由 GraphQL 保留。除了 __typename,GraphQL 服务还提供 __schema__type 元字段,用于公开内省 (introspection) 系统。

指令 (Directives)

我们在上面讨论了变量如何使我们能够避免通过手动字符串插值来构造动态查询。在参数中传递变量解决了这类问题中的大部分,但我们也可能需要一种方法来使用变量动态改变查询的结构和形状。例如,我们可以想象一个具有摘要和详细视图的 UI 组件,其中一个比另一个包含更多字段。

让我们为这样的组件构造一个查询

操作
变量 (Variables)
响应

尝试编辑上面的变量,将 withFriends 改为传递 true,看看结果如何变化。

我们需要使用 GraphQL 中一个叫做指令的功能。具体来说,可执行指令可以由客户端附加到字段或片段包含上,并可以以服务器期望的任何方式影响查询的执行。核心 GraphQL 规范恰好包含两个指令,任何符合规范的 GraphQL 服务器实现都必须支持它们:

  • @include(if: Boolean) 仅当参数为 true 时,才在结果中包含此字段。
  • @skip(if: Boolean) 如果参数为 true,则跳过此字段。

指令在某些情况下非常有用,否则你可能需要通过字符串操作在查询中添加或删除字段。服务器实现也可以通过定义全新的指令来添加实验性功能。

正在寻找有关如何定义可用于标注 GraphQL Schema 中类型、字段或参数的指令的信息?请参阅 Schema 和类型页面,了解更多关于定义和使用类型系统指令的信息。

后续步骤

回顾一下我们学到的关于查询的知识

  • 读取数据的 GraphQL 操作从 query 根操作类型开始,并遍历选择集中的字段,直到叶子值,即标量或枚举类型
  • 字段可以接受改变该字段输出的参数
  • 操作可以使用 querymutationsubscription 关键字来指示其类型
  • 操作类型关键字仅对某些查询操作可以省略
  • 应为操作赋予唯一的名称,这使请求更具表达力并有助于调试
  • 字段别名允许你重命名响应键,在同一个查询中多次包含同一个字段,并为别名后的字段提供不同的参数
  • 变量以 $ 字符开头,可用于为字段参数提供动态值
  • 片段是可重用的字段选择集,可以根据需要在多个查询中使用
  • 可执行指令可以应用于查询,以便在服务器端执行时更改 GraphQL 查询的结果
  • 所有符合规范的 GraphQL 服务器都包含内置指令 @include@skip

现在我们已经了解了如何通过查询操作从 GraphQL 服务器读取数据的方方面面,是时候学习如何使用变更 (mutations) 来更改数据并触发副作用了。

下一课

变更 (Mutations)

探索如何通过变更来修改数据,包括如何通过你的 Schema 更新和删除记录。

前往下一课 教程