graphql schema 学习笔记
graphql schema
在本页,你将学到关于 GraphQL 类型系统中所有你需要了解的知识,以及类型系统如何描述可以查询的数据。
类型系统(Type System)
因为一个 GraphQL 查询的结构和结果非常相似,因此即便不知道服务器的情况,你也能预测查询会返回什么结果。
但是一个关于我们所需要的数据的确切描述依然很有意义,我们能选择什么字段?服务器会返回哪种对象?这些对象下有哪些字段可用?这便是引入 schema 的原因。
每一个 GraphQL 服务都会定义一套类型,用以描述你可能从那个服务查询到的数据。每当查询到来,服务器就会根据 schema 验证并执行查询。
对象类型和字段(Object Types and Fields)
一个 GraphQL schema 中的最基本的组件是对象类型,它就表示你可以从服务上获取到什么类型的对象,以及这个对象有什么字段。
使用 GraphQL schema language,我们可以这样表示它:
type Character {
name: String!
appearsIn: [Episode!]!
}
虽然这语言可读性相当好,但我们还是一起看看其用语,以便我们可以有些共通的词汇:
Character
是一个 GraphQL 对象类型,表示其是一个拥有一些字段的类型。你的 schema 中的大多数类型都会是对象类型。name
和appearsIn
是Character
类型上的字段。这意味着在一个操作Character
类型的 GraphQL 查询中的任何部分,都只能出现name
和appearsIn
字段。String
是内置的标量类型之一 —— 标量类型是解析到单个标量对象的类型,无法在查询中对它进行次级选择。后面我们将细述标量类型。String!
表示这个字段是非空的,GraphQL 服务保证当你查询这个字段后总会给你返回一个值。在类型语言里面,我们用一个感叹号来表示这个特性。[Episode!]!
表示一个Episode
数组。因为它也是非空的,所以当你查询 appearsIn 字段的时候,你也总能得到一个数组(零个或者多个元素)。且由于Episode!
也是非空的,你总是可以预期到数组中的每个项目都是一个Episode
对象。
现在你知道一个 GraphQL 对象类型看上去是怎样,也知道如何阅读基础的 GraphQL 类型语言了。
参数(Arguments)
GraphQL 对象类型上的每一个字段都可能有零个或者多个参数,例如下面的 length 字段:
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
所有参数都是具名的,不像 JavaScript 或者 Python 之类的语言,函数接受一个有序参数列表,而在 GraphQL 中,所有参数必须具名传递。本例中,length 字段定义了一个参数,unit。
参数可能是必选或者可选的,当一个参数是可选的,我们可以定义一个默认值 —— 如果 unit 参数没有传递,那么它将会被默认设置为 METER。
查询和变更类型(The Query and Mutation Types)
你的 schema 中大部分的类型都是普通对象类型,但是一个 schema 内有两个特殊类型:
schema {
query: Query
mutation: Mutation
}
每一个 GraphQL 服务都有一个 query 类型,可能有一个 mutation 类型。这两个类型和常规对象类型无差,但是它们之所以特殊,是因为它们定义了每一个 GraphQL 查询的入口。因此如果你看到一个像这样的查询:
query {
hero {
name
}
droid(id: "2000") {
name
}
}
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
变更也是类似的工作方式 —— 你在 Mutation 类型上定义一些字段,然后这些字段将作为 mutation 根字段使用,接着你就能在你的查询中调用。
有必要记住的是,除了作为 schema 的入口,Query 和 Mutation 类型与其它 GraphQL 对象类型别无二致,它们的字段也是一样的工作方式。
标量类型(Scalar Types)
一个对象类型有自己的名字和字段,而某些时候,这些字段必然会解析到具体数据。这就是标量类型的来源:它们表示对应 GraphQL 查询的叶子节点。
下列查询中,name 和 appearsIn 字段将解析到标量类型:
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
我们知道这些字段没有任何次级字段 —— 因为让它们是查询的叶子节点。
GraphQL 自带一组默认标量类型:
- Int:有符号 32 位整数。
- Float:有符号双精度浮点值。
- String:UTF‐8 字符序列。
- Boolean:true 或者 false。
- ID:ID 标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID 类型使用和 String 一样的方式序列化;然而将其定义为 ID 意味着并不需要人类可读型。
大部分的 GraphQL 服务实现中,都有自定义标量类型的方式。例如,我们可以定义一个 Date 类型:
scalar Date
然后就取决于我们的实现中如何定义将其序列化、反序列化和验证。例如,你可以指定 Date 类型应该总是被序列化成整型时间戳,而客户端应该知道去要求任何 date 字段都是这个格式。
枚举类型(Enumeration Types)
也称作枚举(enum),枚举类型是一种特殊的标量,它限制在一个特殊的可选值集合内。
这让你能够:
- 验证这个类型的任何参数是可选值的某一个
- 与类型系统沟通,一个字段总是一个有限值集合的其中一个值。
下面是一个用 GraphQL schema 语言表示的 enum 定义:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
这表示无论我们在 schema 的哪处使用了 Episode,都可以肯定它返回的是 NEWHOPE、EMPIRE 和 JEDI 之一。
注意,各种语言实现的 GraphQL 服务会有其独特的枚举处理方式。对于将枚举作为一等公民的语言,它的实现就可以利用这个特性;而对于像 JavaScript 这样没有枚举支持的语言,这些枚举值可能就被内部映射成整数值。当然,这些细节都不会泄漏到客户端,客户端会根据字符串名称来操作枚举值。
列表和非空(Lists and Non-Null)
对象类型、标量以及枚举是 GraphQL 中你唯一可以定义的类型种类。但是当你在 schema 的其他部分使用这些类型时,或者在你的查询变量声明处使用时,你可以给它们应用额外的类型修饰符来影响这些值的验证。
我们先来看一个例子:
type Character {
name: String!
appearsIn: [Episode]!
}
此处我们使用了一个 String 类型,并通过在类型名后面添加一个感叹号!将其标注为非空。这表示我们的服务器对于这个字段,总是会返回一个非空值,如果它结果得到了一个空值,那么事实上将会触发一个 GraphQL 执行错误,以让客户端知道发生了错误。
query DroidById($id: ID!) {
droid(id: $id) {
name
}
}
{
"id": null
}
结果:
{
"errors": [
{
"message": "Variable \"$id\" of non-null type \"ID!\" must not be null.",
"locations": [
{
"line": 1,
"column": 17
}
]
}
]
}
列表的运作方式也类似:我们也可以使用一个类型修饰符来标记一个类型为 List,表示这个字段会返回这个类型的数组。在 GraphQL schema 语言中,我们通过将类型包在方括号([ 和 ])中的方式来标记列表。列表对于参数也是一样的运作方式,验证的步骤会要求对应值为数组。
非空和列表修饰符可以组合使用。例如你可以要求一个非空字符串的数组:
myField: [String!]
这表示数组本身可以为空,但是其不能有任何空值成员。用 JSON 举例如下:
myField: null // 有效
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 错误
然后,我们来定义一个不可为空的字符串数组:
myField: [String]!
这表示数组本身不能为空,但是其可以包含空值成员:
myField: null // 错误
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 有效
你可以根据需求嵌套任意层非空和列表修饰符。
接口(Interfaces)
跟许多类型系统一样,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
,这都属于特定的类型的角色。
当你要返回一个对象或者一组对象,特别是一组不同的类型时,接口就显得特别有用。
注意下面例子的查询会产生错误:
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
primaryFunction
}
}
{
"ep": "JEDI"
}
返回结果:
{
"errors": [
{
"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
原因是:
hero 字段返回 Character 类型,取决于 episode 参数,它可能是 Human 或者 Droid 类型。上面的查询中,你只能查询 Character 接口中存在的字段,而其中并不包含 primaryFunction。
如果要查询一个只存在于特定对象类型上的字段,你需要使用内联片段:
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
}
}
{
"ep": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
联合类型(Union Types)
联合类型和接口十分相似,但是它并不指定类型之间的任何共同字段。
union SearchResult = Human | Droid | Starship
在我们的schema中,任何返回一个 SearchResult 类型的地方,都可能得到一个 Human、Droid 或者 Starship。注意,联合类型的成员需要是具体对象类型;你不能使用接口或者其他联合类型来创造一个联合类型。
这时候,如果你需要查询一个返回 SearchResult 联合类型的字段,那么你得使用内联片段才能查询任意字段。
{
search(text: "an") {
__typename
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
结果
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo",
"height": 1.8
},
{
"__typename": "Human",
"name": "Leia Organa",
"height": 1.5
},
{
"__typename": "Starship",
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
_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!
输入类型(Input Types)
目前为止,我们只讨论过将例如枚举和字符串等标量值作为参数传递给字段,但是你也能很容易地传递复杂对象。这在变更(mutation)中特别有用,因为有时候你需要传递一整个对象作为新建对象。在 GraphQL schema language 中,输入对象看上去和常规对象一模一样,除了关键字是 input 而不是 type:
input ReviewInput {
stars: Int!
commentary: String
}
你可以像这样在变更(mutation)中使用输入对象类型:
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
结果
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
输入对象类型上的字段本身也可以指代输入对象类型,但是你不能在你的 schema 混淆输入和输出类型。输入对象类型的字段当然也不能拥有参数。