GraphQL LogoGraphQL

模式和类型

在本页中,您将了解有关 GraphQL 类型系统及其如何描述可查询数据的全部知识。由于 GraphQL 可与任何后端框架或编程语言一起使用,因此我们将避免实现特定的细节,只讨论概念。

类型系统#

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

  1. 我们从一个特殊的“根”对象开始
  2. 我们选择该对象上的 hero 字段
  3. 对于 hero 返回的对象,我们选择 nameappearsIn 字段

由于 GraphQL 查询的形状与结果非常匹配,因此您可以在不知道太多服务器信息的情况下预测查询将返回什么。但是,拥有对我们可以请求的数据的精确描述非常有用 - 我们可以选择哪些字段?它们可能返回哪些类型的对象?这些子对象上有哪些字段可用?这就是模式发挥作用的地方。

每个 GraphQL 服务都定义了一组类型,这些类型完全描述了您可以在该服务上查询的可能数据的集合。然后,当查询进来时,它们会根据该模式进行验证和执行。

类型语言#

GraphQL 服务可以用任何语言编写。由于我们不能依赖特定的编程语言语法(如 JavaScript)来讨论 GraphQL 模式,因此我们将定义我们自己的简单语言。我们将使用“GraphQL 模式语言” - 它类似于查询语言,并允许我们以与语言无关的方式讨论 GraphQL 模式。

对象类型和字段#

GraphQL 架构的最基本组件是对象类型,它代表你可以从服务中获取的一种对象类型,以及它包含的字段。在 GraphQL 架构语言中,我们可以这样表示它

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

这种语言非常易读,但让我们回顾一下,以便我们拥有共同的词汇

  • Character 是一个 GraphQL 对象类型,这意味着它是一种具有某些字段的类型。你架构中的大多数类型都将是对象类型。
  • nameappearsInCharacter 类型上的 字段。这意味着 nameappearsIn 是在任何对 Character 类型进行操作的 GraphQL 查询中可以出现的唯一字段。
  • String 是内置的 标量 类型之一 - 这些类型解析为单个标量对象,并且在查询中不能有子选择。我们稍后将详细介绍标量类型。
  • String! 表示该字段是 非空 的,这意味着 GraphQL 服务承诺在查询此字段时始终为你提供一个值。在类型语言中,我们将用感叹号来表示它们。
  • [Episode!]! 表示 Episode 对象的 数组。由于它也是 非空 的,因此你始终可以期望在查询 appearsIn 字段时得到一个数组(包含零个或多个项目)。由于 Episode! 也是 非空 的,因此你始终可以期望数组中的每个项目都是一个 Episode 对象。

现在你知道了 GraphQL 对象类型的样子,以及如何阅读 GraphQL 类型语言的基础知识。

参数#

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

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

所有参数都是命名的。与 JavaScript 和 Python 等语言不同,这些语言中的函数采用有序参数列表,GraphQL 中的所有参数都是通过名称专门传递的。在本例中,length 字段有一个定义的参数 unit

参数可以是必需的或可选的。当参数是可选时,我们可以定义一个 默认值 - 如果没有传递 unit 参数,它将默认设置为 METER

查询和变异类型#

大多数模式中的类型都是普通的对象类型,但模式中还有两种特殊的类型。

schema {
query: Query
mutation: Mutation
}

每个 GraphQL 服务都有一个 query 类型,并且可能存在或不存在 mutation 类型。这些类型与普通对象类型相同,但它们很特殊,因为它们定义了每个 GraphQL 查询的入口点。因此,如果您看到类似于以下的查询:

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

type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}

Mutations 的工作方式类似 - 您在 Mutation 类型上定义字段,这些字段可用作您可以在查询中调用的根突变字段。

重要的是要记住,除了作为模式的“入口点”的特殊状态之外,QueryMutation 类型与任何其他 GraphQL 对象类型相同,它们的字段工作方式完全相同。

标量类型#

GraphQL 对象类型具有名称和字段,但在某些时候,这些字段必须解析为一些具体数据。这就是标量类型发挥作用的地方:它们代表查询的叶子。

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

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

GraphQL 自带一组默认的标量类型。

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

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

scalar Date

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

枚举类型#

枚举类型,也称为 Enums,是一种特殊的标量类型,它被限制为一组特定的允许值。这使您可以

  1. 验证此类型的所有参数是否都是允许的值之一
  2. 通过类型系统传达一个字段将始终是一组有限值中的一个

以下是枚举定义在 GraphQL 模式语言中的示例

enum Episode {
NEWHOPE
EMPIRE
JEDI
}

这意味着,无论我们在模式中使用 Episode 类型,我们都期望它恰好是 NEWHOPEEMPIREJEDI 之一。

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

列表和非空#

对象类型、标量和枚举是您可以在 GraphQL 中定义的唯一类型。但是,当您在模式的其他部分或查询变量声明中使用这些类型时,您可以应用额外的 类型修饰符,这些修饰符会影响这些值的验证。让我们看一个例子

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

在这里,我们使用 String 类型,并在类型名称后添加感叹号 ! 来将其标记为 非空。这意味着我们的服务器始终期望为该字段返回一个非空值,如果最终得到一个空值,实际上会触发一个 GraphQL 执行错误,让客户端知道出现了一些问题。

非空类型修饰符也可以在定义字段参数时使用,如果在 GraphQL 字符串或变量中传递空值作为该参数,则会导致 GraphQL 服务器返回验证错误。

列表的工作方式类似:我们可以使用类型修饰符将类型标记为 List,这表示该字段将返回该类型的数组。在模式语言中,这用方括号 [] 括起类型来表示。对于参数来说,它也是一样的,验证步骤将期望该值是一个数组。

非空和列表修饰符可以组合使用。例如,您可以拥有一个非空字符串列表

myField: [String!]

这意味着列表本身可以为空,但它不能包含任何空成员。例如,在 JSON 中

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

现在,假设我们定义了一个非空字符串列表

myField: [String]!

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

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

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

接口#

与许多类型系统一样,GraphQL 支持接口。接口是一种抽象类型,它包含一组特定的字段,类型必须包含这些字段才能实现该接口。

例如,您可以有一个接口 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,这意味着它可能是 HumanDroid,具体取决于 episode 参数。在上面的查询中,您只能查询 Character 接口上存在的字段,而该接口不包含 primaryFunction

要查询特定对象类型的字段,您需要使用内联片段

在查询指南的 内联片段 部分了解更多信息。

联合类型#

联合类型与接口类似;但是,它们无法在组成类型之间定义任何共享字段。

union SearchResult = Human | Droid | Starship

在我们的模式中,无论何时返回 SearchResult 类型,我们都可能得到 HumanDroidStarship。请注意,联合类型的成员需要是具体的对象类型;您不能使用接口或其他联合类型创建联合类型。

在这种情况下,如果您查询返回 SearchResult 联合类型的字段,则需要使用内联片段才能查询任何字段

__typename 字段解析为 String,它允许您在客户端区分不同的数据类型。

此外,在这种情况下,由于 HumanDroid 共享一个公共接口 (Character),因此您可以在一个地方查询它们的公共字段,而不是在多个类型中重复相同的字段

{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}

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

输入类型#

到目前为止,我们只讨论了将标量值(如枚举或字符串)作为参数传递给字段。但您也可以轻松地传递复杂对象。这在突变的情况下特别有用,您可能希望传入一个要创建的完整对象。在 GraphQL 模式语言中,输入类型看起来与常规对象类型完全相同,但使用 input 关键字而不是 type

input ReviewInput {
stars: Int!
commentary: String
}

以下是如何在突变中使用输入对象类型

输入对象类型上的字段本身可以引用输入对象类型,但您不能在模式中混合输入类型和输出类型。输入对象类型也不能在其字段上具有参数。

继续阅读 →验证