GraphQL: 查询(queries)和变更(mutations)
GraphQL是一种用于API的查询语言。在服务器端运行时,它使用数据定义的类型系统来执行查询。GraphQL不限于特定的数据库或存储引擎。它是一种可以与现有代码和数据配合使用的后端。
本文是根据GraphQL官方的「查询和变更」解释进行的重新说明,并用日语进行了重新解释。这不是福建译文,因此省略了原文中的描述部分,并对理解困难的部分进行了补充。
领域 yù)
GraphQL是用于获取对象字段的查询语言。例如,下面的查询用于获取英雄的姓名。
{
hero {
name
}
}
然后,通过相同的形状(shape),从查询中获得结果。在GraphQL中,服务器能够理解客户端的需求,并返回相应的字段。在这个例子中,字段name的类型是String,返回的是“R2-D2”,即星球大战中的英雄。
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
顺便说一句,GraphQL官方网站上的代码示例是互动性环境的。您可以重新编写代码并验证结果,务必尝试一下(例如,在字段name后面加上appearsIn,可以得到出现的次数)。
在前面提到的代码示例中,通过查询英雄的name字段返回的是一个字符串。但是,作为字段,也可以引用对象。在这种情况下,您可以选择对象中的字段。在GraphQL查询中,您可以横向遍历相关对象和它们的字段,并在一个请求中获取相关的数据。这与以前使用REST进行重复查询的设计方法有所不同。
{
hero {
name
# クエリにはコメントも加えられる
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
请注意这段代码中的friends字段返回的是一个项目数组。在GraphQL查询中,单个项和多个列表被视为相同。选择返回哪种取决于模式的指定。
参数
在GraphQL中,除了可以跨对象和字段获取数据之外,还可以向字段传递参数。
{
human(id: "1000") {
name
height
}
}
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
类似于REST的系统只能传递一组参数。这是通过查询参数和URL进行请求的方式。而在GraphQL中,每个字段甚至嵌套对象都可以分别传递参数。这样多个API的加载就可以合并为一个。如果将参数传递给标量字段,则可以在服务器上一次性完成数据转换的实现,而无需在每个客户端上分开实现。
{
human(id: "1000") {
name
height(unit: FOOT)
}
}
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}
在参数中,有各种不同的类型。在这个代码示例中,使用的是枚举类型。它表示了一个固定选项(在这个例子中是长度单位METER或FOOT)中的一个选项。GraphQL具有默认的类型。然而,GraphQL服务器可以声明自己的类型,只要它可以在传输格式中进行序列化(有关GraphQL的类型系统,请参阅”类型系统”)。
化名 (huà
在以前的代码示例中,查询的结果是根据字段名返回的。如果在同一个字段上给出不同的参数,则可能无法一次性提取出多个结果,因为它们会重叠。这时就可以使用别名。通过给查询字段指定另一个名称,可以获得以该名称命名的结果。
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
在这段代码中,我使用了不同的参数对同一个hero字段发出了请求。但是,由于我给它取了一个别名,所以结果可以一次性提取出来。
碎片
当应用程序需要的数据增加时,查询也会变得复杂。当多个查询使用相同的一部分字段组合时,可能会希望重复使用它们。将这样的字段组合提取出来并定义它们称为“片段”。
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
即使在这个比较数据的例子中,你也可以看出如果不使用片段,GraphQL的代码会变得冗长。片段有助于将复杂的应用程序数据请求分解并重复使用。通过将多个UI组件的初始数据分成片段,您也可以一次性获取它们。
在片段中使用变量
フラグメントは、GraphQLドキュメントのクエリや変更に宣言された変数を参照できます(後で説明する「変数」を参照)。- 转述
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
}
}
}
}
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"friendsConnection": {
"totalCount": 4,
"edges": [
{
"node": {
"name": "Han Solo"
}
},
{
"node": {
"name": "Leia Organa"
}
},
{
"node": {
"name": "C-3PO"
}
}
]
}
},
"rightComparison": {
"name": "R2-D2",
"friendsConnection": {
"totalCount": 3,
"edges": [
{
"node": {
"name": "Luke Skywalker"
}
},
{
"node": {
"name": "Han Solo"
}
},
{
"node": {
"name": "Leia Organa"
}
}
]
}
}
}
}
在这个代码示例中,对于查询HeroComparison的变量$first给定了默认值3,因此将每个3个元素的数组传递给片段comparisonFields的friendsConnection,并显示在edges数组中。
操作名称
使用上述的变量的语法为查询指定了一个名称(HeroComparison)。在关键字query之后附加的标识符是操作名称。在简化语法中,可以省略它们。然而,在公开的应用程序中,建议使用它们来清晰地表示代码。例如,如果将操作名称(HeroNameAndFriends)添加到前面提到的”字段”部分中提供的GraphQL代码中,它将如下所示(结果相同,省略)。
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
操作类型指示了操作的目的,并且可以使用以下三个关键字。当想要为操作提供变量时,操作类型和操作名称是必需的,就像前面的代码示例一样。
-
- query
-
- mutation
- subscription
操作名称提供了对操作的明确含义。在GraphQL的多操作文档中是必需的。即使不是必需的,它也对于调试和服务器端日志非常有用。当网络或GraphQL服务器出现错误或其他问题时,如果为查询和更新提供了名称,就可以在不必仔细检查内容的情况下确定请求。
变量
到目前为止,参数基本上都是在查询字符串中进行编写的。然而,在许多应用程序中,传递给字段的参数将是动态的。例如,从下拉菜单中选择星球大战的剧集,或者确定字段的搜索和过滤条件。在这种情况下,我们将使用变量作为参数传递。具体步骤如下:
1.
-
- 将想要在查询中动态提供的(静态的)值替换为变量($variableName)。
-
- 在查询中声明并接收变量($variableName)。
- 通过变量字典(通常为JSON)传递额外的变量值(variableName: value)以进行传输。
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
"episode": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
通过使用变量,就不再需要根据修改后的值在客户端代码中重新创建查询字符串的工作。此外,也可以明确哪些参数值会动态更改。
定义变量
变量使用前缀$来定义。然后变量名后面跟着一个冒号(:),表示数据类型。
$episode: Episode
能够以变量的形式声明的有以下3种类型。
-
- スカラー
-
- 列挙型
- 入力オブジェクト型
当您希望将复杂对象传递到字段时,必须在服务器上知道匹配的输入对象类型。
在定义变量时,有些情况下可以省略,有些情况下则是必需的。如果在前述类型定义”Episode”后没有感叹号符号”!”的话,这意味着可以省略。而如果要传递给变量的字段需要非空参数,则不能省略变量。
默认变量值
在给查询的变量设置默认值时,请在类型定义后面使用等号运算符添加。
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
只要每个变量都有默认值,就可以调用查询而不需要传递值。当某些变量在变量字典中传递值时,它们会覆盖默认值。
指示
通过使用变量,可以实现动态查询,无需手动转换查询字符串。将变量作为参数传递是一种非常有效的方法。此外,还可以使用变量动态地改变查询的结构和形状。例如,当需要显示不同字段数量的摘要或详细信息的UI组件时。
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
{
"episode": "JEDI",
"withFriends": false
}
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
因为将false传递给变量withFriends,所以结果中不会显示字段friends。如果将值更改为true,则friends字段将包含在结果中。
{
"episode": "JEDI",
"withFriends": true
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
@include(if: $withFriends)是GraphQL中用来指定friends字段的一个指令。指令可以应用在字段或者片段上,并且可以改变向服务器查询的结果。GraphQL核心规范内部有两个内建指令,这些指令在符合GraphQL规范的服务器实现中得到支持。
@include(if: Boolean) – 引数がtrueのときフィールドが結果に含まれる。
@skip(if: Boolean) – 引数がtrueのときフィールドが結果から除かれる。
通过指令,可以在不重写查询字符串的情况下添加或删除字段。通过在服务器实现指令,还可以添加新功能。
变异
在GraphQL中讨论的话题通常涉及到数据的获取。但是作为一个数据平台,我们可能也希望修改服务器端的数据。
在REST中,请求可能会对服务器产生某种副作用。当这种情况发生时,通常建议在GET请求中不修改数据。同样,在GraphQL中,建议通过明确的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!"
}
}
}
请注意createReview字段返回了新创建的review的stars和commentary。这在修改已存在的数据时非常方便。例如,当您想要增加字段时,您可以通过一个请求同时进行修改和获取新值。
在这个代码示例中,你可能注意到传递的变量review不是标量。这个变量是一个输入对象类型。它是一个特殊的对象类型,可以作为参数传递。
修改多个字段
与查询(query)一样,还可以包含对多个字段的更改。查询(query)和更改(mutation)之间除了名称之外还存在一个重要的区别。查询的字段是并行执行的,而更改是依次处理每个字段的。
这样一来,如果在一次请求中向字段 incrementCredits 发送了两次变更,确保第一次重写完成后才会开始第二次重写。这样可以避免由于顺序混乱而导致意外结果的发生。
行内片段
与许多其他类型系统一样,GraphQL架构中具有定义接口和联合类型的功能。当对返回接口或联合类型的字段执行查询时,需要使用内联片段根据包含的具体类型引用数据。以下代码是一个例子:
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
{
"ep": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
这个查询返回的是Character类型的hero。根据参数episode,它可能是Droid或Human之一。当直接选择时,只能查询Character接口中提供的字段,比如name。
当需要具体类型的字段时,必须使用带有类型条件的内联片段。第一个片段被标记为“… on Droid”,所以只有当hero返回的Character是Droid类型时,primaryFunction字段才会被执行。
{
"ep": "EMPIRE"
}
{
"data": {
"hero": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
当Character的类型是Human时,通过第二个片段,字段将变成height。
对于带有名称的片段,处理方式不会改变。这是因为带有名称的片段必然会添加类型。
元数据字段
有时我们可能不知道从GraphQL服务返回的数据类型。为了解决这个问题,我们希望知道客户端应该如何处理这些数据。在GraphQL中,我们可以使用元字段__typename来请求这个信息。通过这个字段,我们可以得到查询中任何位置的对象名称。
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}
在这个代码示例中的search查询中,将返回三个类型条件中的其中一个的联合类型。通过__typename字段,客户端可以知道不同类型的差异。
GraphQL服务中还有一些其他元字段。它们用于公开内省系统。
GraphQL系列的基础
「GraphQL: 查询和变更」
「GraphQL: 模式和类型」
「GraphQL: 验证」
「GraphQL: 执行」
「GraphQL: 自省」