Schema 和类型

了解 GraphQL 类型系统的不同元素

GraphQL 类型系统描述了可以从 API 查询哪些数据。这些能力的集合被称为服务的 schema(模式),客户端可以使用该 schema 向 API 发送查询,并获得可预测的结果。

在本页中,我们将探讨 GraphQL 的 六种命名类型定义 以及类型系统的其他特性,了解如何使用它们来描述数据及其相互关系。由于 GraphQL 可以与任何后端框架或编程语言配合使用,我们将避免具体的实现细节,仅讨论概念。

类型系统

如果你以前见过 GraphQL 查询,你就会知道 GraphQL 查询语言基本上是关于在对象上选择字段。例如,在以下查询中:

操作
响应
  1. 我们从一个特殊的“根”对象开始
  2. 我们选择其中的 hero 字段
  3. 对于 hero 返回的对象,我们选择 nameappearsIn 字段

由于 GraphQL 查询的形状与结果密切匹配,我们可以在不了解服务器太多信息的情况下预测查询将返回什么。但是,对我们可以请求的数据有一个精确的描述是非常有用的。例如,我们可以选择哪些字段?它们可能返回哪种类型的对象?这些子对象上有哪些可用字段?

这就是 schema 发挥作用的地方。每个 GraphQL 服务都定义了一组类型,完整地描述了在该服务上可以查询的所有可能数据。然后,当请求进入时,它们将根据该 schema 进行验证和执行。

类型语言

GraphQL 服务可以用任何语言编写,在定义 schema 中的类型时可以采取许多不同的方法

  • 一些库让你使用编写 GraphQL 实现的同种编程语言,将 schema 类型、字段和解析器(resolver)函数构造在一起。
  • 一些库允许你使用通常称为 schema 定义语言(或 SDL)的方式更符合人体工程学地定义类型和字段,然后单独为相应的字段编写解析器函数。
  • 一些库允许你编写并注释解析器函数,然后从中推断出 schema。
  • 一些库甚至可能根据某些底层数据源为你自动推断出类型和解析器函数。

由于在本指南中我们不能依赖特定的编程语言来讨论 GraphQL schema,我们将使用 SDL,因为它与我们目前看到的查询语言相似,并且允许我们以一种与语言无关的方式讨论 GraphQL schema。

对象类型和字段

GraphQL schema 最基本的组件是 对象类型 (Object types),它代表了你可以从服务中获取的一种对象及其拥有的字段。在 SDL 中,我们这样表示:

type Character {
  name: String!
  appearsIn: [Episode!]!
}

这种语言非常易读,但让我们过一遍,以便我们拥有共同的词汇:

  • Character 是一个 GraphQL 对象类型,意味着它是一个拥有字段的类型。你 schema 中的大多数类型都会是对象类型。
  • nameappearsInCharacter 类型上的 字段。这意味着 nameappearsIn 是在对 Character 类型进行操作的 GraphQL 查询中唯一可以出现的字段。
  • String 是内置的 标量类型 (Scalar types) 之一。这些类型解析为单个标量值,并且在查询中不能有子选择。稍后我们将更详细地讨论标量类型。
  • String! 表示该字段是 非空类型 (Non-Null type),意味着 GraphQL 服务承诺在你查询此字段时总会给你一个值。在 SDL 中,我们用感叹号表示。
  • [Episode!]! 表示 Episode 对象的 列表类型 (List type)。当列表是非空时,查询 appearsIn 字段时你总是可以期待一个数组(包含零个或多个项目)。在这种情况下,由于 Episode! 在列表内也是非空的,你可以期待数组中的每一项都是一个 Episode 对象。

现在你知道了 GraphQL 对象类型的样子以及如何阅读基本的 SDL。

参数

GraphQL 对象类型上的每个字段都可以有零个或多个 参数,例如下面的 length 字段:

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

所有参数都是命名的。不像 JavaScript 和 Python 等语言中函数接受有序参数列表,GraphQL 中的所有参数都专门通过名称传递。在这种情况下,length 字段有一个名为 unit 的定义参数。

参数可以是必填的或可选的。当参数可选时,我们可以定义一个 默认值。如果未传递 unit 参数,则默认设置为 METER

Query、Mutation 和 Subscription 类型

每个 GraphQL schema 都必须支持 query 操作。这个 根操作类型入口点 是一个默认名为 Query 的常规对象类型。所以如果你看到一个如下所示的查询:

操作
响应

这意味着 GraphQL 服务需要有一个带有 droid 字段的 Query 类型:

type Query {
  droid(id: ID!): Droid
}

Schema 还可以通过添加额外的 MutationSubscription 类型,并在相应的根操作类型上定义字段,来支持 mutationsubscription 操作。

重要的是要记住,除了作为 schema 入口点的特殊地位外,QueryMutationSubscription 类型与任何其他 GraphQL 对象类型相同,它们的字段工作方式也完全相同。

你也可以为根操作类型命名为不同的名字;如果你选择这样做,你需要使用 schema 关键字通知 GraphQL 新名称:

schema {
  query: MyQueryType
  mutation: MyMutationType
  subscription: MySubscriptionType
}

标量类型

GraphQL 对象类型有名称和字段,但最终这些字段必须解析为某些具体的数据。这就是 标量类型 (Scalar types) 发挥作用的地方:它们代表查询的叶子节点值。

在以下查询中,nameappearsIn 字段将解析为标量类型:

操作
响应

我们知道这一点是因为这些字段没有任何子字段——它们是查询的叶子。

GraphQL 开箱即用地带有一组 默认标量类型

  • Int:有符号 32 位整数。
  • Float:有符号双精度浮点值。
  • String:UTF‐8 字符序列。
  • Booleantruefalse
  • ID:唯一标识符,通常用于重新获取对象或作为缓存的键。 ID 类型的序列化方式与 String 相同;但是,将其定义为 ID 意味着它不打算让人类可读。

在大多数 GraphQL 服务实现中,还有一种指定自定义标量类型的方法。例如,我们可以定义一个 Date 类型:

scalar Date

然后由我们的实现来定义该类型应如何序列化、反序列化和验证。例如,你可以指定 Date 类型应始终序列化为整数时间戳,并且你的客户端应该知道任何日期字段都符合该格式。

枚举类型

枚举类型 (Enum types),也称为枚举,是一种特殊的标量,它被限制在一组特定的允许值内。这允许你:

  1. 验证此类型的任何参数是否为允许值之一
  2. 通过类型系统传达:某个字段将始终是有限值集中的一个

这是 SDL 中枚举类型定义的样子:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

这意味着无论我们在 schema 中的何处使用 Episode 类型,我们都期望它正是 NEWHOPEEMPIREJEDI 之一。

各种语言的 GraphQL 服务实现将有特定于语言的处理枚举类型的方法。在支持枚举作为一等公民的语言中,实现可能会利用这一点;在像 JavaScript 这样没有枚举支持的语言中,这些值可能会在内部映射到一组整数。但是,这些细节不会泄露给客户端,客户端可以完全根据枚举类型值的字符串名称进行操作。

类型修饰符

在 GraphQL 中,默认情况下类型被假定为可为空且单数的。但是,当你在 schema 中(或在 查询变量声明 中)使用这些命名类型时,你可以应用额外的 类型修饰符,这将影响这些值的含义。

正如我们在上面的对象类型示例中看到的,GraphQL 支持两种类型修饰符——列表 (List)非空 (Non-Null) 类型——它们可以单独使用或相互组合使用。

非空 (Non-Null)

让我们看一个例子:

type Character {
  name: String!
}

这里,我们正在使用 String 类型,并通过在类型名称后添加感叹号 (!) 将其标记为非空类型。这意味着我们的服务器始终期望为此字段返回一个非空值,如果解析器产生了一个空值,那么将触发 GraphQL 执行错误,让客户端知道出了问题。

正如我们在上面的示例中看到的,在为字段定义参数时也可以使用非空类型修饰符,如果将空值作为该参数传递,这将导致 GraphQL 服务器返回验证错误:

操作
响应

列表 (List)

列表以类似的方式工作。我们可以使用类型修饰符将类型标记为列表类型,这表示该字段将返回该类型的数组。在 SDL 中,这通过将类型包装在方括号 [] 中来表示。它对参数的工作方式相同,验证步骤将期望该值是一个数组。这是一个例子:

type Character {
  name: String!
  appearsIn: [Episode]!
}

正如我们上面所见,非空和列表修饰符可以组合。例如,你可以拥有一个非空 String 类型的列表:

myField: [String!]

这意味着 列表本身 可以为空,但它不能有任何空元素。例如,在 JSON 中:

myField: null // valid
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // error

现在,假设我们定义了一个 String 类型的非空列表:

myField: [String]!

这意味着列表本身不能为 null,但它可以包含 null 值:

myField: null // error
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // valid

最后,你还可以拥有一个非空 String 类型的非空列表:

myField: [String!]!

这意味着列表既不能为 null,也不能包含 null 值:

myField: null // error
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // error

你可以根据需要任意嵌套任意数量的非空和列表修饰符。

在 GraphQL 中,没有办法定义一种类型使得字段的数据只有在提供非空列表时才被认为是有效的。换句话说,对于非空类型的非空列表,[] 仍然是一个有效的响应。

接口类型

像许多类型系统一样,GraphQL 支持 抽象类型。我们将探讨的第一种抽象类型是 接口类型 (Interface type),它定义了一组特定的字段,具体的对象类型或其他接口类型必须包含这些字段才能实现它。

例如,你可以有一个 Character 接口类型,代表星球大战三部曲中的任何角色:

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

这意味着任何 实现 Character 的类型都需要具有这些完全相同的字段,以及相同的参数和返回类型。

例如,以下是一些可能实现 Character 的类型:

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}
 
type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

你可以看到这两种类型都具有 Character 接口类型的所有字段,但也引入了额外的字段——totalCreditsstarshipsprimaryFunction——这些是特定于该特定角色类型的。

当你想要返回一个对象或一组对象,但这些对象可能属于几种不同的类型时,接口类型非常有用。例如,请注意以下查询会产生错误:

操作
响应

hero 字段返回 Character 类型,这意味着根据 episode 参数,它可能是 HumanDroid。在上面的查询中,你只能请求 Character 接口类型上存在的字段,其中不包括 primaryFunction

要请求特定对象类型上的字段,你需要使用 内联片段 (inline fragment)

操作
响应

接口类型也可以实现其他接口类型:

interface Node {
  id: ID!
}
 
interface Character implements Node {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

请注意,接口类型不得实现自身,也不得相互包含任何循环引用。

虽然在 GraphQL API 中类型可能会共享公共字段,但并不总是需要使用接口类型来强制这些字段保持一致命名。接口类型是跨实现它们的类型指示共享行为的一种强大方式。在使用时,它们应该对客户端开发人员具有语义意义,就像 Character 是人类和机器人的有用抽象一样。

联合类型

GraphQL 支持第二种抽象类型,称为 联合类型 (Union type)。联合类型与接口类型有相似之处,但它们不能在成员类型之间定义任何共享字段。

联合类型通过指定其成员对象类型来定义:

union SearchResult = Human | Droid | Starship

无论我们在 schema 中的何处返回 SearchResult 类型,我们都可能得到 HumanDroidStarship。请注意,联合类型的成员必须是具体的对象类型;你不能使用接口类型或其他联合类型作为成员来定义它。

在这种情况下,如果你查询一个返回 SearchResult 联合类型的字段,你需要使用 内联片段 来查询成员对象类型上定义的任何字段:

操作
响应

__typename 字段是一个特殊的 元字段 (meta-field),它自动存在于每个对象类型上,并解析为该类型的名称,从而提供了一种在客户端区分数据类型的方法。

此外,在这种情况下,由于 HumanDroid 共享一个公共接口 (Character),你可以在一个地方查询它们的公共字段,并仍然得到相同的结果:

操作
响应

请注意,name 仍然在 Starship 上指定,因为否则它不会出现在结果中,因为 Starship 不是 Character

输入对象类型

我们在本页介绍的大多数示例都演示了如何将对象、标量、枚举、接口和联合类型用作 schema 中字段的 输出类型。但我们也看到字段参数必须指定它们的 输入类型

到目前为止,我们只谈到了使用标量值(如枚举或字符串类型)作为字段参数的输入类型。但是,你也可以使用 输入对象类型 (Input Object type) 将复杂对象作为参数传递,这是我们将探讨的 GraphQL 中最后一种命名类型。

这在 mutation (变更) 的情况下特别有价值,因为你可能想要传入一个要创建的完整对象。在 SDL 中,输入对象类型看起来与常规对象类型相似,但使用关键字 input 而不是 type

input ReviewInput {
  stars: Int!
  commentary: String
}
 
type Mutation {
  createReview(episode: Episode, review: ReviewInput!): Review
}

以下是你如何在 mutation 中使用输入对象类型:

操作
响应

输入对象类型上的字段可以引用其他输入对象类型,但你不能在 schema 中混用输入和输出类型。输入对象类型上的字段也不能有参数。

指令

在某些情况下,字段参数不足以满足需求,或者某些共同行为必须在多个位置复制,指令 (directives) 允许我们通过使用 @ 字符后跟指令名称来修改 GraphQL schema 或操作的某些部分。

类型系统指令 允许我们注释 schema 中的类型、字段和参数,以便它们可以以不同的方式被验证或执行。

指令也可以被定义为在 GraphQL 操作中使用的 可执行指令在“查询”页面阅读更多关于可执行指令的内容。

GraphQL 规范定义了几种 内置指令。例如,对于支持 SDL 的实现,可以使用 @deprecated 指令来注释 schema 中已弃用的部分:

type User {
  fullName: String
  name: String @deprecated(reason: "Use `fullName`.")
}

虽然如果你使用的 GraphQL 实现支持 SDL,你不需要在 schema 中显式定义 @deprecated 指令,但它的底层定义看起来像这样:

directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

请注意,就像字段一样,指令可以接受参数,并且这些参数可以有默认值。@deprecated 指令有一个可为空的 reason 参数,它接受一个 String 作为输入类型,并回退到 "No longer supported"。对于指令,我们必须指定它们可以在哪里使用,例如 @deprecated 指令的 FIELD_DEFINITIONENUM_VALUE

除了 GraphQL 的内置指令外,你还可以定义自己的 自定义指令。与自定义标量类型一样,这取决于你选择的 GraphQL 实现来确定在查询执行期间如何处理自定义指令。

文档

描述

GraphQL 允许你向 schema 中的类型、字段和参数添加文档。事实上,GraphQL 规范鼓励你在所有情况下都这样做,除非类型、字段或参数的名称是自我描述的。Schema 描述使用 Markdown 语法定义,可以是多行或单行。

在 SDL 中,它们看起来像这样:

"""
A character from the Star Wars universe
"""
type Character {
  "The name of the character."
  name: String!
}
 
"""
The episodes in the Star Wars trilogy
"""
enum Episode {
  "Star Wars Episode IV: A New Hope, released in 1977." 
  NEWHOPE
  "Star Wars Episode V: The Empire Strikes Back, released in 1980."
  EMPIRE
  "Star Wars Episode VI: Return of the Jedi, released in 1983."
  JEDI
}
 
"""
The query type, represents all of the entry points into our object graph
"""
type Query {
  """
  Fetches the hero of a specified Star Wars film.
  """
  hero(
    "The name of the film that the hero appears in."
    episode: Episode
  ): Character
}

除了使 GraphQL API schema 更有表现力之外,描述对客户端开发人员也很有帮助,因为它们在 内省查询 (introspection queries) 中可用,并且在诸如 GraphiQL 之类的开发人员工具中可见。

注释

有时,你可能需要在 schema 中添加一些不描述类型、字段或参数且不打算让客户端看到的注释。在这些情况下,你可以通过在文本前添加 # 字符向 SDL 添加单行注释:

# This line is treated like whitespace and ignored by GraphQL
type Character {
  name: String!
}

注释也可以添加到客户端查询中:

操作
响应

后续步骤

回顾一下我们学到的关于 schema 和类型的知识:

  • 取决于选择哪个库来构建 GraphQL 服务,我们可以使用 SDL 以一种与语言无关的方式定义 schema,或者通过从为其字段提供数据的代码编译出 schema
  • GraphQL 中有六种命名类型定义:对象 (Object)、标量 (Scalar)、枚举 (Enum)、接口 (Interface)、联合 (Union) 和输入对象 (Input Object) 类型
  • 对象类型包含指定输出类型的字段,这些字段可能具有指定输入类型的参数
  • IntFloatStringBooleanID 标量类型内置于 GraphQL 中,你也可以定义自己的自定义标量
  • 与标量类型一样,枚举类型代表 GraphQL schema 中的叶子值,但它们仅限于一组有限的值
  • 列表 ([]) 和非空 (!) 类型修饰符允许你更改字段输出类型或参数输入类型的默认行为
  • 接口和联合类型是抽象类型,允许从单个字段输出不同的具体对象类型
  • 输入对象类型允许你向字段参数或指令参数传递比标量和枚举类型更复杂的值
  • 类型系统指令可以应用于 schema 中的类型、字段和参数,以改变它们在查询期间的验证和执行方式
  • GraphQL 支持使用类型、字段和参数描述的 schema 文档,它还支持被解析器忽略的注释

现在你已经了解了类型系统的关键特性,你已经准备好学习更多关于如何 从 GraphQL API 查询数据 的内容了。

下一课

查询 (Queries)

了解如何构造 GraphQL 查询以准确请求所需的数据——包括字段、变量和片段。

前往下一课 教程