GraphQL 入门

GraphQL是一个用于API的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。
GraphQL并乜有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

查询和变更

字段 - Fields

查询和其结果拥有几乎一样的结构。这是GraphQL最重要的特性
总是能得到你想要的数据,而服务器也准确地知道客户端请求的字段。

字段也能指代对象类型(Object),可以对这个对象的字段进行次级选择(sub-selection)
GraphQL查询能够遍历相关对象及其字段,使得客户端可以一次请求查询大量相关数据

GraphQL查询会同等看待单个项目或者一个列表的项目,可以通过schema所指示的内容来预测将会得到哪一种

{
  hero {
    name
    # 查询可以有备注!
    friends {
      name
    }
  }
}

参数 - Arguments

在GraphQL中,每一个字段和嵌套对象都能有自己的一组参数,从而使得GraphQL可以完美替代多次API获取请求

甚至你也可以给标量(scalar)字段传递参数,用于实现服务端的一次转换,而不用每个客户端分别转换。
例如通过指定单位的方式,针对一个接口服务,不同的请求参数可以获取到不同单位的数据

参数可以是多种不同的类型。可以使用一个枚举类型,代表了一个有限选项集合。
GraphQL自带一套默认类型,但是GraphQL服务器可以声明一套自己的定制类型,只要能序列化成你的传输格式即可。
[[GraphQL Learn#Schema和类型]]

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}

别名 - Aliases

重命名结果中的字段为任意你想的名字。

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

片段 - Fragments

片段使你能够组织一组字段,然后在需要它们的地方引入
可复用单元

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

片段内使用变量

片段可以访问查询或变更中声明的变量

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

操作名称 - Operation name

下面示例包含了query-操作类型,HeroNameAndFields-操作名称

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

操作类型
可以是query,mutation或者subscription
描述打算做什么类型的操作

操作名称
操作的有意义和明确的名称
仅在有多个操作的文档中时必需的。
鼓励使用 - 对于调试和服务器端日志记录非常有用

变量 - Variables

GraphQL拥有一级方法将动态值提取到查询之外,然后作为奋力的字段传进去,这些动态值即称为变量

使用变量之前,得做三件事:

  1. 使用$variableName 替代查询中的静态值
  2. 声明$variableName为查询接受的变量之一
  3. $variableName: value通过传输专用的分离的变量字典中
# { "graphiql": true, "variables": { "episode": JEDI } }
query HeroNameAndFriends($episode: Episode) {
 hero(episode: $episode) {
	 name
	 friends {
		 name
	 }
 }
}

查询的参数将是动态的,决不能使用用户提供的值来字符串插值以构建查询。

变量定义 - Variable definitions

($episode: Episode)
工作方式跟类型语言中函数的参数定义一样
变量前缀必须为$,后跟其类型

所有声明的变量都必须是标量、枚举型或者输入对象类型

变量定义可以是可选的或者必要的。
Episode后并没有!,表示其可选的。

变量定义的句法,可以看[[GraphQL Learn#Schema和类型]]

默认变量 - Default variables

可以通过在查询中的类型定义后面附带默认值的方式,将默认值赋给变量

query HeroNameAndFriends($episode: Episode = "JEDI") {
 hero(episode: $episode) {
	...
 }
}

指令 - Directives

需要一个方式使用变量动态地改变我们查询的结构。
一个指令可以附着在字段或者片段包含的字段上,然后任何服务端期待的方式来改变查询的执行。

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

GraphQL的核心规范包含两个指令,其必须被任何规范兼容的GraphQL服务器实现所支持:

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

服务端实现也可以定义新的指令来添加新的特性。

变更 - Mutations

规范任何导致写入的操作都应该显式通过变更(mutation)来发送。

调用时的入参是用来写入数据的,{...}中的内容是声明执行完后返回的内容

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

如果一个字段自增的时候,可以在一个请求中变更并查询这个字段的新值

变更中的多个字段 - Multiple fields in mutaions

查询和变更之间的区别:

  • 名称
  • 执行顺序
    • 查询字段,并行执行
    • 变更字段,线性执行,一个接着一个

内联片段 - Inline Fragments

[[GraphQL Learn#Schema和类型#接口 - interface|Schema]] 具备定义接口和联合类型的能力。

查询的字段返回的是接口或者联合类型,可能需要使用内联片段来取出下层具体类型的数据。
可以先阅读[[GraphQL Learn#接口 - interface|接口]]

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
// variable
{
  "ep": "JEDI"
}
// request
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

可以根据当前返回的对象的子类型进行判断展示,
如果返回的是Droid类型,则展示primaryFucntion字段。
如果返回的是Human类型,则展示height字段。

具名片段可以用于同样的情况,因为具名片段总是附带一个类型

具名片段是啥

元字段 - Meta fields

某些情况下,客户端并不知道从GraphQL服务获得什么类型,但又需要通过类型决定如何处理数据。GraphQL允许在查询的任何位置请求__typename
一个元字段,以获得哪个位置的对象类型名称

Graphql服务提供了不少元字段,可以查询[[GraphQL Learn#内省]]

Schema和类型

  • GraphQL类型系统
  • 类型系统如何描述可以查询的数据
  • 屏蔽实现上的细节而仅仅专注于其概念

类型系统 - Type System

GraphQL查询语言基本上就是关于选择对象上的字段。

{
  hero {
    name
    appearsIn
  }
}
  1. 以一个特殊的对象root开始,完整的应该有操作名称
  2. 选择其上的hero字段
  3. 对于hero返回的对象,我们选择nameappearsIn字段

每一个GraphQL服务都会定义一套类型,用以描述可能从服务查询到的数据。
每当查询到来,服务器会根据schema验证big执行查询。

类型语言 - Type Language

定义自己的简单语言,称之为“GraphQL schema language” —— 和GraphQL的查询语言很相似

对象类型和字段

最基本的组件是对象类型, 表示可以从服务商获取到什么类型的对象,以及这个对象有什么字段

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

  • Character是一个GraphQL对象类型,表示其是一个拥有一些字段的类型
  • nameappearsInCharacter类型上的字段。意味着在一个操作Character类型的GraphQL查询中的任何部分,都只能出现nameappearsIn字段
  • String是内置的标量类型之一
    • 标量类型是解析到单个标量对象的类型,无法在查询中对它进行次级选择
  • String!表示这个字段是非空的,保证当你查询这个字段后会给你返回一个值
  • [Episode!]!表示一个Episode数组
    • appearsIn总能得到一个数组,零个或者多个元素
    • Episode!也是非空的,可以得到数组中的
    • 疑问:这是返回空数组还是肯定有一个对象的空数组
      • 参见:[[GraphQL Learn#组合使用]]

参数 - Arguments

对象类型上的每一个字段都可能有零个或者多个参数

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

所有参数都是具名的,必须指定类型。
参数可能是必选或者可选的,但一个参数是可选的,可以定义一个默认值,如果unit参数没有传递,默认设置称为METER

查询和变更类型 - The Query and Mutation Types

在Schema中有两个特殊的类型:query和mutation

所有GraphQL服务都会有query类型,但不一定会有mutation类型。
这两个特殊的原因他们界定了所有GraphQL查询的入口

Query

query {
	hero{
		name
	}
	droid(id: "2000") {
		name
	}
}

如果GraphQL需要使用上面的查询herodroid,需要有一个Query类型包含了这两个字段:

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

Mutation

整体与query相类似。
需要在Mutation类型中定义字段,这样在你调用接口时才能作为变更的根节点字段被使用到。

注意:QueryMutation除了作为Schema的入口,其他和正常的对象类型一样。

标量类型 - Scalar Types

GraphQL对象类型有名称和字段,有一些需要映射到一些具体的数值,这就引出了标量。
像基础类型
映射到所有返回结果的叶子字段上,因此他们没有子字段。

GraphQL有一组开箱即用的默认标量类型:

  • Int:有符号的32位整数
  • Float:有符号的双精度浮点值
  • String:UTF-8字符串
  • Booleantrue或者false
  • ID:代表一个唯一标识,通常用于取回一个对象或者作为缓存的key。
    • 序列化方式与String一样
    • 定义成ID意味着它不是human-readable

GraphQL服务的大多数实现中,都有方式自定义特殊的标量类型

scalar Date

具体取决于实现:如何序列化、反序列化和验证。

比如可以将Date类型指定成序列化为整数时间戳,同时客户端需要知道所有Date类型都是返回这个格式的。

枚举类型 - Enumeration Types

标量的特殊类型
限制了特定的允许值的集合。
可以用作:

  1. 验证任何这个类型的参数是被允许的值
  2. 告诉类型系统,这个字段是一个有限集合的值
enum Episode {
	NEWHOPE
	EMPIRE
	JEDI
}

任何使用了Episode类型的schema,都可以认为它是上面三个的其中一个。

不同的语言实现时会有不同的实现方式来处理enum。但是细节不需要透露给客户端,客户端可以按照字符串来操作枚举值。

列表和非空 - Lists and Non-Null

对象类型、标量类型和枚举只有这几种是你可以在GraphQL中定义的。
但是当你在其他的schema或者查询变量声明时,可以使用额外的类型修饰符,它可以影响那那些值的校验。

Non-Null

通过在类型名称后面添加一个感叹号标志表示这个字段是非空的的。
意味着从服务端查询时,这个字段一定会返回一个非空(Non-Null)的值
如果反悔了一个null,会触发GraphQL的异常报错,让客户端知道有问题

非空修饰符也可以用在定义字段的参数,这样如果一个null值作为参数传到了服务端,会返回一个验证错误。

Lists

用房类似,使用类型修饰符表示一种作为List的类型,表示字段会范围这个类型的数组。
在schema语言中,通过被中括号包裹来表示,[]
这个也可以用在参数中,检验的步骤会验证是那个类型值的数组。

组合使用

非空和列表可以组合使用。

例一:

myField: [String!]

表示列表可以为空,但是列表中的元素不能为空

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

例二:

myFiled: [String]!

表示列表本身不能为空,到那时可以包含空值

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

接口 - interface

GraphQL支持接口
接口是一个抽象类型,包含了特定的字段集合。
实现时必须包含这些字段

例子

有一个Character接口来代表星战三部曲中的任何角色

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

意味着实现了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接口中的所有字段,同时增加了额外的字段,它们具体到了特殊的角色类型。

当需要返回有多种类型的对象或对象集合时,接口非常有用。

为了获取到特殊对象类型的字段,需要使用[[GraphQL Learn#内联片段 - Inline Fragments|内联片段]]。

联合类型 - Union Types

联合类型和接口非常相似,但是它们没有不指定类型之间的公共字段

union SearchResult = Human | Droid | Starship

在我们的schema中任何返回了searchResult的地方,都有可能得到HumanDroid或者Starship
注意的是联合类型的成员必须是具体的对象类型,不能是接口或者是其他的联合类型

例如:

{
  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,它可以让你在客户端上区分每一个数据类型之间的差别。

如果HumanDroid公用了一个通用的接口(Character),可以这样查询:

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

注意的是name对于Starship仍然需要指定的,因为它不是Character

输入类型 - Input Types

确切地说,输入类型和常规的对象类型一样,但是它的关键字是input,而不是type
它在变更场景下的特别有价值,可能想要传递整个创建的对象。

input ReviewInput {
	stars: Int!
	commentary: String
}

输入类型的字段可以引用输入类型,但是不能将输入类型和输出类型混起来使用。
输入独享类型的字段上不能有参数

为啥需要区分出input type

规范中GraphQL认为对象类型不使用作为输入类型重复使用
对象类型可以包含定义参数或者包含引用到接口或者联合类型的字段,这些作为输入参数中使用并不不合适。

What's the point of input type in GraphQL?

验证

使用这个类型系统,可以提前知道一个GraphQL请求是不是有效的。

一个[[GraphQL Learn#片段 - Fragments|片段]]不能引用自己的类型,或者创建一个循环。
因为可能会导致无限的结果。

当我们查询字段值时,必须查询提供的类型中存在的字段。
如果查找类型中不存在的字段,查询是无效的。

当我们查询非标量和枚举类型的字段时,需要指定我们想要这个字段的哪些数据。
比如对于对象类型,作为叶子节点进行查询,查询会返回异常

相类似的是,如果一个字段时标量,那么不能在它基础上查询额外字段。

对于接口类型实现的其他类型,在查询时,可以通过[[GraphQL Learn#内联片段 - Inline Fragments|内联片段]]的方式进行指定类型的查询。
通过设置片段界定Droid下才展示,我们需要确认我们只在定义了这个字段的类型下请求它。

{
  hero {
    name
    ...DroidFields
  }
}

fragment DroidFields on Droid {
  primaryFunction
}

这样看起来有点冗长,上面通过[[GraphQL Learn#片段 - Fragments|片段]]的方式,可以达到复用的效果。如果只使用一次,可以使用没有名称的独立片段


这只是验证系统的冰山一角;事实上需要一大套验证规则才能保证 GraphQL 查询的语义意义

执行

内省

Reference

GraphQL 入门
Introduction to GraphQL - 上面的中文翻译阅读上存在理解偏差建议阅读原文