GraphQL: 模式与类型

本文是根据官方网站“Schemas and Types”的内容,对GraphQL类型系统以及如何描述可以请求的数据进行解释。GraphQL可以用于任何后端框架和编程语言,因此我们将关注思维方式而避免具体的实现细节。这不是文档的翻译,而是在日语中重新解释。有些部分被省略了,而某些难以理解的地方则进行了补充。另外,GraphQL官方网站的代码示例是一个交互环境。您可以重写代码并确认结果,所以请务必尝试一下。

类型系统

GraphQL查询语言的基础是选择对象的字段。让我们通过以下查询代码示例来思考,例如:

{
	hero {
		name
		appearsIn
	}
}
{
	"data": {
		"hero": {
			"name": "R2-D2",
			"appearsIn": [
				"NEWHOPE",
				"EMPIRE",
				"JEDI"
			]
		}
	}
}
    1. 从特殊的根对象开始。

 

    1. 选择其下的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类型始终序列化为整数时间戳。客户端可以了解日期字段的格式是什么样的。

枚举类型

枚举类型是一种特殊的标量,它限制了取值的组合。可以进行以下操作:

    1. 通过类型系统确定该参数是允许的某个值。

 

    通过类型系统,告知该字段是特定值组合中的一个。

以下是使用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: 内省」

广告
将在 10 秒后关闭
bannerAds