GraphQL: 模式与类型
本文是根据官方网站“Schemas and Types”的内容,对GraphQL类型系统以及如何描述可以请求的数据进行解释。GraphQL可以用于任何后端框架和编程语言,因此我们将关注思维方式而避免具体的实现细节。这不是文档的翻译,而是在日语中重新解释。有些部分被省略了,而某些难以理解的地方则进行了补充。另外,GraphQL官方网站的代码示例是一个交互环境。您可以重写代码并确认结果,所以请务必尝试一下。
类型系统
GraphQL查询语言的基础是选择对象的字段。让我们通过以下查询代码示例来思考,例如:
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
-
- 从特殊的根对象开始。
-
- 选择其下的hero字段。
从hero返回的对象中选择name和appearsIn。
GraphQL的查询形状是对所需结果的几乎原封不动的描述。即使对服务器了解不多,也可以预测查询会返回什么。然而,有一个精确的数据描述可以方便地了解可以要求什么。可以选择哪些字段?将返回什么样的对象?可以从子对象中提取哪些字段?这就是模式的用途。
在GraphQL服务中,通过定义类型的组合,清晰地描述了可以获取哪些数据。当有查询请求时,将根据模式进行验证和执行。
语言类型
GraphQL服务可以使用任何语言编写。解释GraphQL模式时,不应该基于特定编程语言(如JavaScript)的语法。因此,我们需要定义一种自己的简易的“GraphQL模式语言”。这种语言类似于查询语言,容易解释GraphQL模式,且不受特定语言的限制。
对象类型和字段
GraphQL的基本要素是对象类型。它表示可从服务获取的对象,并指示包含哪些字段。以下代码是GraphQL模式语言的示例。
type Character {
name: String!
appearsIn: [Episode!]!
}
让我们逐个检查这段代码的元素。
Character – GraphQLのオブジェクト型で、いくつかのフィールドをもつ。スキーマの型の多くはオブジェクト型。
nameおよびappearsIn – Characterがもつフィールド。Character型を操作するGraphQLクエリは、このふたつのフィールドだけが扱える。
String! – nullではない文字列スカラー型。
String – 文字列として解決される組み込みスカラー型のひとつ。単独のスカラーオブジェクトなので、クエリでフィールドの中の入れ子の選択はできない。
! – nullではない(non-nullable)フィールドを示す。GraphQLサービスでこのフィールドのクエリは必ず値が返される。
[Episode!]! – Episodeオブジェクトの配列で、nullではない。したがって、appearsInフィールドのクエリは必ず配列(0以上の要素を含む)を返す。Episode!もnullではないので、要素は必ずEpisodeオブジェクトでなければならない。
以上是GraphQL类型语言的基础。
参数
在GraphQL对象类型的每个字段都可以提供参数,如果需要的话。例如,在下面的代码示例中,”unit”就是作为参数传递给了”length”字段。
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
在GraphQL中,每个参数都有一个名称。与使用JavaScript或Python等语言的函数按顺序接收参数的方式不同,GraphQL的参数都是通过特定的名称来传递的。在这个代码示例中,length字段具有一个名为unit的特定参数。
参数可以是必填的,也可以是可省略的。只有当参数是可选时,才能给定默认值。如果没有传递参数到length字段,则unit将被设置为默认值METER。
问题类型和变异类型
通常情况下,模式中的大部分类型都是常规对象类型。然而,这个模式中还有两种特殊类型。
schema {
query: Query
mutation: Mutation
}
所有的GraphQL服务都有Query类型。Mutation类型可以有,也可以没有。这两个类型与普通的对象类型相同。然而,可以说,它们是指定GraphQL查询入口点的特殊类型。让我们看下面的代码示例。
query {
hero {
name
}
droid(id: "2000") {
name
}
}
{
"data": {
"hero": {
"name": "R2-D2"
},
"droid": {
"name": "C-3PO"
}
}
}
这个GraphQL服务的查询类型必须具有两个字段:hero和droid。
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
Mutation类型也可以以同样的方式处理。在Mutation类型中定义的字段可以在调用查询的修改字段的根中使用。
除了在模式中扮演“入口点”的特殊位置之外,查询和变更都与其他GraphQL对象类型没有区别。可以以完全相同的方式操作指定的字段。
标量类型
GraphQL对象具有名称和字段,并且可以进一步进行嵌套。然而,最终字段必须解析为具体的数据。这被称为标量类型。它是查询的终点。在下面的代码示例中,字段名称和出现次数将被解析为标量类型。
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
在GraphQL中,标量类型没有任何字段,因为它们是查询树的叶子节点。GraphQL默认包含以下标量类型。
Int – 符号つき32ビット整数。
Float – 符号つき倍精度浮動小数点値。
String – UTF-8文字列。
Boolean – trueかfalseかの真偽値。
ID – 一意の識別子を表す。オブジェクトの再読み込みやキャッシュのキーなどに用いられる。文字列と同じようにシリアル化されるが、人が読めることを意図しない。
在大多数GraphQL服务中,已经实现了定义自定义标量类型的功能。例如,可以定义日期类型。
scalar Date
型的序列化、反序列化和验证方式是由实现决定的。例如,可以规定将Date类型始终序列化为整数时间戳。客户端可以了解日期字段的格式是什么样的。
枚举类型
枚举类型是一种特殊的标量,它限制了取值的组合。可以进行以下操作:
-
- 通过类型系统确定该参数是允许的某个值。
- 通过类型系统,告知该字段是特定值组合中的一个。
以下是使用GraphQL模式语言定义枚举类型的示例代码。
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
如果在这种情况下,使用模式(Schema)与Episode作为类型,那么我们可以假设NEWHOPE,EMPIRE,JEDI中的任一个。
请注意,不同语言实现的GraphQL服务对于每种语言都有自己处理枚举类型的方式。支持正式枚举类型的语言可能会将其纳入实现中。对于不支持枚举类型的语言,如JavaScript,可能会考虑将其作为整数组处理。然而,这些细节对于客户端是不可见的。枚举类型的值可以通过名称字符串进行操作。
列表和非空类型修饰符
在GraphQL中,只有对象类型、标量类型和枚举类型是可以定义的。但是,在模式的其他部分或查询变量的声明中使用的类型,可以添加类型修饰符来影响验证。下面的代码是一个例子。
type Character {
name: String!
appearsIn: [Episode]!
}
字段名为String类型,在类型名称后面使用感叹号!指定为非空(Non-Null)。服务器不允许返回该字段的null值。如果返回的值为null,则会发生GraphQL执行错误。这样客户端就会被通知到发生了问题。
在定义字段参数时,可以使用非空修饰符!。当GraphQL字符串或变量参数传递了空值时,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
}
]
}
]
}
在中国,您可以使用相同的方式使用类型修饰符来表示列表。从使用列表修饰符的字段中,将返回该类型的数组。在模式语言中,可以使用角括号[]来表示列表。它还可以用作参数,并且在验证阶段,值必须是一个数组。
Non-Null修飾子和列表修饰子可以结合使用。例如,下面的代码是定义Non-Null字符串列表的一个例子。
myField: [String!]
列表本身可以为null,空列表也是允许的。但是,不能拥有null成员。以下代码示例是关于JSON的。
myField: null // 有効
myField: [] // 有効
myField: ['a', 'b'] // 有効
myField: ['a', null, 'b'] // エラー
根据需要,可以将非空修饰符和列表修饰符嵌套多层。
接口
与许多其他类型系统一样,GraphQL也支持接口。接口是抽象类型,在实现时必须具备特定字段的组合。
例如,下面的代码是表示《星球大战》三部曲角色的接口Character的例子。
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
实现Character的类型必须准确地具备这些字段,每个字段都需要具有相应的参数和返回值类型。
例如,以下是一种能够实现Character类型的Human和Droid的示例。
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接口所定义的所有字段。此外,Human拥有starships和totalCredits,而Droid具有primaryFunction,这些都是它们各自实现Character接口之外的独有附加字段。
当你想要返回对象或其组合时,使用接口会很有用。然而,可能存在实现了同一接口但具有不同类型的情况。例如,下面的代码查询会导致错误。
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
}
]
}
]
}
英雄字段返回Character类型。根据参数episode的不同,它可以是Human类型或Droid类型。在这个代码示例的查询中,只能查询Character接口定义的字段。换句话说,primaryFunction字段不包含在内。
以下是错误消息的结果。特定对象类型(Droid)的字段必须使用内联片段进行查询。
无法在类型为”Character”的字段上查询”primaryFunction”。你是否打算在”Droid”上使用内联片段?
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
}
}
{
"ep": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
请阅读「GraphQL: 查询和变更」中的「内联片段」,以获取更详细的信息。
联合型
联合类型有一些与接口类似的特点,但是它不能指定类型之间的共同字段。
union SearchResult = Human | Droid | Starship
在模式中返回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类型,在客户端上可以相互区分不同的数据类型。
顺便说一下,Character是Human和Droid共同实现的接口。因此,共同的字段不需要从多种类型中各自获取,而是可以一次性地集中在一个地方进行查询(结果与上述代码示例相同)。
{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}
请注意,在这个代码示例中,内联片段的Starship有一个指定的name。由于Starship没有实现Character接口,如果不包含name,它就不会显示在结果中。
输入类型
过去我们提到的是将枚举类型、字符串等标量作为参数传递给字段。但是,即使是复杂的对象也可以轻松传递。特别有用的是在需要更改时,您可能想要将整个创建的对象传递过去。在GraphQL模式语言中,输入类型看起来与普通对象完全相同。不同之处在于使用input关键字代替type。
input ReviewInput {
stars: Int!
commentary: String
}
以下是使用输入对象类型进行更改的代码。
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!"
}
}
}
输入对象类型的字段可以进一步引用自身的输入对象类型。但是,在模式中输入类型和输出类型不能混合使用。此外,输入对象类型的字段不能接受参数。
GraphQL系列的基本概念
「GraphQL: 查询和变更」
「GraphQL: 模式和类型」
「GraphQL: 验证」
「GraphQL: 执行」
「GraphQL: 内省」