在本页中,您将了解有关 GraphQL 类型系统及其如何描述可查询数据的全部知识。由于 GraphQL 可与任何后端框架或编程语言一起使用,因此我们将避免实现特定的细节,只讨论概念。
如果您以前见过 GraphQL 查询,您就会知道 GraphQL 查询语言基本上是关于选择对象上的字段。例如,在以下查询中
hero
字段hero
返回的对象,我们选择 name
和 appearsIn
字段由于 GraphQL 查询的形状与结果非常匹配,因此您可以在不知道太多服务器信息的情况下预测查询将返回什么。但是,拥有对我们可以请求的数据的精确描述非常有用 - 我们可以选择哪些字段?它们可能返回哪些类型的对象?这些子对象上有哪些字段可用?这就是模式发挥作用的地方。
每个 GraphQL 服务都定义了一组类型,这些类型完全描述了您可以在该服务上查询的可能数据的集合。然后,当查询进来时,它们会根据该模式进行验证和执行。
GraphQL 服务可以用任何语言编写。由于我们不能依赖特定的编程语言语法(如 JavaScript)来讨论 GraphQL 模式,因此我们将定义我们自己的简单语言。我们将使用“GraphQL 模式语言” - 它类似于查询语言,并允许我们以与语言无关的方式讨论 GraphQL 模式。
GraphQL 架构的最基本组件是对象类型,它代表你可以从服务中获取的一种对象类型,以及它包含的字段。在 GraphQL 架构语言中,我们可以这样表示它
type Character { name: String! appearsIn: [Episode!]!}
这种语言非常易读,但让我们回顾一下,以便我们拥有共同的词汇
Character
是一个 GraphQL 对象类型,这意味着它是一种具有某些字段的类型。你架构中的大多数类型都将是对象类型。name
和 appearsIn
是 Character
类型上的 字段。这意味着 name
和 appearsIn
是在任何对 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 服务需要有一个带有 hero
和 droid
字段的 Query
类型。
type Query { hero(episode: Episode): Character droid(id: ID!): Droid}
Mutations 的工作方式类似 - 您在 Mutation
类型上定义字段,这些字段可用作您可以在查询中调用的根突变字段。
重要的是要记住,除了作为模式的“入口点”的特殊状态之外,Query
和 Mutation
类型与任何其他 GraphQL 对象类型相同,它们的字段工作方式完全相同。
GraphQL 对象类型具有名称和字段,但在某些时候,这些字段必须解析为一些具体数据。这就是标量类型发挥作用的地方:它们代表查询的叶子。
在以下查询中,name
和 appearsIn
字段将解析为标量类型。
我们知道这一点,因为这些字段没有任何子字段 - 它们是查询的叶子。
GraphQL 自带一组默认的标量类型。
Int
:一个带符号的 32 位整数。Float
:一个带符号的双精度浮点数。String
:一个 UTF-8 字符序列。Boolean
:true
或 false
。ID
:ID 标量类型表示一个唯一标识符,通常用于重新获取对象或作为缓存的键。ID 类型以与 String 相同的方式序列化;但是,将其定义为 ID
表示它不打算为人阅读。在大多数 GraphQL 服务实现中,还有一种方法可以指定自定义标量类型。例如,我们可以定义一个 Date
类型。
scalar Date
接下来,由我们的实现来定义该类型应该如何序列化、反序列化和验证。例如,您可以指定 Date
类型应始终序列化为整数时间戳,并且您的客户端应知道任何日期字段都应使用该格式。
枚举类型,也称为 Enums,是一种特殊的标量类型,它被限制为一组特定的允许值。这使您可以
以下是枚举定义在 GraphQL 模式语言中的示例
enum Episode { NEWHOPE EMPIRE JEDI}
这意味着,无论我们在模式中使用 Episode
类型,我们都期望它恰好是 NEWHOPE
、EMPIRE
或 JEDI
之一。
请注意,各种语言中的 GraphQL 服务实现将有其自己的语言特定方式来处理枚举。在支持枚举作为一等公民的语言中,实现可能会利用这一点;在像 JavaScript 这样的没有枚举支持的语言中,这些值可能会在内部映射到一组整数。但是,这些细节不会泄露给客户端,客户端可以完全根据枚举值的字符串名称进行操作。
对象类型、标量和枚举是您可以在 GraphQL 中定义的唯一类型。但是,当您在模式的其他部分或查询变量声明中使用这些类型时,您可以应用额外的 类型修饰符,这些修饰符会影响这些值的验证。让我们看一个例子
type Character { name: String! appearsIn: [Episode]!}
在这里,我们使用 String
类型,并在类型名称后添加感叹号 !
来将其标记为 非空。这意味着我们的服务器始终期望为该字段返回一个非空值,如果最终得到一个空值,实际上会触发一个 GraphQL 执行错误,让客户端知道出现了一些问题。
非空类型修饰符也可以在定义字段参数时使用,如果在 GraphQL 字符串或变量中传递空值作为该参数,则会导致 GraphQL 服务器返回验证错误。
列表的工作方式类似:我们可以使用类型修饰符将类型标记为 List
,这表示该字段将返回该类型的数组。在模式语言中,这用方括号 [
和 ]
括起类型来表示。对于参数来说,它也是一样的,验证步骤将期望该值是一个数组。
非空和列表修饰符可以组合使用。例如,您可以拥有一个非空字符串列表
myField: [String!]
这意味着列表本身可以为空,但它不能包含任何空成员。例如,在 JSON 中
myField: null // validmyField: [] // validmyField: ["a", "b"] // validmyField: ["a", null, "b"] // error
现在,假设我们定义了一个非空字符串列表
myField: [String]!
这意味着列表本身不能为空,但它可以包含空值
myField: null // errormyField: [] // validmyField: ["a", "b"] // validmyField: ["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
接口中的所有字段,但也引入了特定于该特定角色类型的额外字段,totalCredits
、starships
和 primaryFunction
。
当您想要返回一个对象或一组对象,但这些对象可能属于几种不同类型时,接口很有用。
例如,请注意以下查询会产生错误
hero
字段返回类型 Character
,这意味着它可能是 Human
或 Droid
,具体取决于 episode
参数。在上面的查询中,您只能查询 Character
接口上存在的字段,而该接口不包含 primaryFunction
。
要查询特定对象类型的字段,您需要使用内联片段
在查询指南的 内联片段 部分了解更多信息。
联合类型与接口类似;但是,它们无法在组成类型之间定义任何共享字段。
union SearchResult = Human | Droid | Starship
在我们的模式中,无论何时返回 SearchResult
类型,我们都可能得到 Human
、Droid
或 Starship
。请注意,联合类型的成员需要是具体的对象类型;您不能使用接口或其他联合类型创建联合类型。
在这种情况下,如果您查询返回 SearchResult
联合类型的字段,则需要使用内联片段才能查询任何字段
__typename
字段解析为 String
,它允许您在客户端区分不同的数据类型。
此外,在这种情况下,由于 Human
和 Droid
共享一个公共接口 (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}
以下是如何在突变中使用输入对象类型
输入对象类型上的字段本身可以引用输入对象类型,但您不能在模式中混合输入类型和输出类型。输入对象类型也不能在其字段上具有参数。