引用自 “GraphQL入门 – 让你渴望使用GraphQL”
这篇文章是Livesense Advent Calendar 2016 – Qiita中第24天的文章。
大概过了一年后,一开始被认为是REST的下一个趋势!?的GraphQL并没有引起太多的关注。
然而,因为一直很在意这项技术,所以在2016年结束之前,我简略地进行了一番调研。
这篇总结是我在最近两天内快速调查的结果,若有理解或解释上的错误,或者有一些表达意义我没有理解的地方,请不吝赐教。
太长不看
我只关注了查询,完全没有提到变更,所以内容变得非常冗长。实际上,
阅读GraphQL的首页,了解其氛围,
思考为什么GitHub工程需要GraphQL,
进一步了解GraphQL的介绍,
按照GraphQL.js教程开始入门并试着运行一下,
当遇到GraphQL Relay规范 – Relay Docs时,深入一步,
查看GitHub开发者指南上的实际API。
我觉得您大概能理解,所以没有读这篇文章也能达到更确切的理解。
为了避免以文章形式表达,让英语博客和文档阅读困难的人,我会简要总结如下。
首先,要把握氛围。
由于目前Github提供了GraphQL的API(仅限早期访问),所以我打算以此为例来进行解释。
以下是一个查询,用于搜索包含有“GraphQL”这个名称的仓库,并获取相应仓库的数量。
query {
search(query: "GraphQL", type: REPOSITORY) {
repositoryCount
}
}
作为结果,返回了以下的JSON。
{
"data": {
"search": {
"repositoryCount": 2955
}
}
}
让我们看另一个例子。
下面是一个查询,用于获取名为”bananaumai”的登录ID用户并获取其姓名。
query {
user(login: "bananaumai") {
name
}
}
作为结果,将返回以下的JSON格式数据。
{
"data": {
"user": {
"name": "YUTA Shimakawa"
}
}
}
以以下方式将查询放在HTTP的POST请求中提交:1
$ curl -H "Authorization: bearer <<your access token>>" -X POST -d '
{
"query": "query { user(login: \"bananaumai\") { name }}"
}
' https://api.github.com/graphql
为什么要使用GraphQL?
根据GitHub的工程师博客所述,在他们不断扩展RESTful Web API的过程中,逐渐感受到了困难之处,并开始寻找新的范式。在这个过程中,他们遇到了GraphQL,并且目前正在提供早期访问版的信息。GitHub选择GraphQL的理由被介绍如下。
-
- 尝试绘制客户端页面时,需要发出多次HTTP请求的挑战。
-
- 此外,例如,存在对以下API的细微改进的愿望。
希望收集每个API端点所需的Oauth范围。
希望能更智能地对集合类资源进行分页。
希望能够类型安全地处理用户发出的API请求参数。
希望能够从API的实现代码中生成文档。
希望能够自动生成客户端代码。
等等。
我希望你能在下面看到如何使用GraphQL解决像上述这样的问题。
GraphQL 的特点
Schema(API的定义)的存在与查询语言规范相结合。
GraphQL拥有Schema,通过它可以构建基于类型系统的API。
让我们考虑一个简单的GraphQL API架构,它只是简单地返回商店列表。
schema {
type Shop {
id: ID!
name: String!
}
type Query {
shops: [Shop]!
}
}
“Type关键字是用于声明数据类型的”
在这段话中,我们使用”Shop”关键字定义了一个名为Shop的对象类型。其中的”name”关键字被称为字段(和JavaScript一样),在冒号后面紧跟的”String”表示该字段的类型是String值。”!”是NotNull指定关键字,表示该字段不能为空。对象类型可以设置任意数量的字段。而”id”字段是一个特殊的字符串,保证了它的唯一性和无重复性。关于这个字段的详细信息,在需要的情况下可以参考http://graphql.org/learn/schema/#scalar-types。
接下来定义了一个名为 Query 的类型。Query 类型具有一个名为 shops 的字段,这表示它是一个 Shop 类型的列表。同时,Query 类型在 GraphQL 模式中具有特殊的意义,指定为 Query 类型的字段将成为 API 的顶级查询接口。也就是说,在上述例子中,如果 Query 只有一个字段,那么只能执行以下类型的查询。
query {
shops {
id,
name
}
}
以下是返却值的示例。
{
"data" : {
[
{id: "1B814046-86C1-43E7-BE77-443B8F3319C9", "name": "バナナ屋"},
{id: "C0C62859-C5D8-4BFD-B2BB-93574C4B2921", "name": "りんご屋"},
{id: "A5640770-E9B8-4532-AF39-B027BC79A19F", "name": "みかん屋"}
]
}
}
另外,如果用REST API来表示商店资源,一般来说会返回名字和ID两者,但是在GraphQL中,只获取Shop.name这样的操作也是可以的。
query {
shops {
name
}
}
{
"data" : {
[
{"name": "バナナ屋"},
{"name": "りんご屋"},
{"name": "みかん屋"}
]
}
}
接下来,让我们稍微扩展一下这个API。
也可以定义具有参数的字段,如下所示。
schema {
type Shop {...}
type Query {
shops: [Shop]!
shop(name: String): Shop
}
}
如果有提供这样的API,就能够进行以下这样的查询。
query {
shop(name: "バナナ屋") {
name
}
}
结果如下所示。
{
"data" : {
"name": "バナナ屋"
}
}
也可以定义与数据操作相关的API。
schema {
type Shop {...}
type Query {...}
type Mutation {
addShop(shop: ShopInput): Shop
updateShop(id: ID!, shop): ShopInput
}
input ShopInput {
name: Name
}
}
使用Mutation指定一个类型,并定义一个用于操作数据的API作为其字段。另外,关键字”input”用于定义在mutation时用于保存数据的数据。根据这个定义,以下是进行数据更新的步骤。
mutation {
addShop({ name: "いちご屋" }) {
id
}
}
如果数据添加成功,将返回以下数据。
{
"data": {
"addShop": {
"id": "592AC8B4-76A3-417F-B193-97114ECF9CBE"
}
}
}
通过Schema的存在,可以对查询进行类型级别的验证。
正如上述所述,可以通过Schema在类型级别上验证查询的有效性。
例如,GraphQL的GUI客户端GraphiQL可以在查询编辑器内检测到无效的参数或字段,并进行突出显示。可能(虽然没有仔细查看源代码),由于GraphQL的规范定义了访问Schema信息等元信息的API,因此客户端可以使用这些信息来检测Schema并进行类型检查。
在这里我们不再深入讨论,如果您想了解更详细的信息,请参考http://graphql.org/learn/validation/。
可以通过模式来表示对象图
在GraphQL Schema中,如前所述,使用者可以自行定义类型,并且可以将这些类型信息应用于其他类型的定义中。因此,我认为它可以直接将对象关系(图形)作为API来表示。
我想扩展之前仅返回商店列表的GraphQL API,并考虑处理以下模型的API。
如果我们更仔细地考虑这种模型,就会得到以下结果(为了简化起见,在这里我们省略了与数据操作API或关系无关的字段)。
将这个转化为GraphQL后,会得到以下的Schema。
type Shop {
id: ID!
name: String!
items: [Item]
}
type Item {
id: ID!
name: String!
shop: Shop
revs: [Review]
}
type User {
id: ID!
name: String!
revs: [Review]
}
type Review {
id: ID!
content: String!
item: Item
user: User
}
type Query {
shop(name: String): Shop
user(name: String): User
item(id: ID): Item
}
对于这种模式,可以发出如下查询。
query {
shop("バナナ屋"): {
name,
items: {
id,
name,
revs: {
content
}
}
}
}
{
"data": {
"name": "バナナ屋",
"items": [
"id": "DF9B698A-A70E-4DC0-989A-8003A8168E11"
"name": "甘熟王"
"revs": [
{"content": "うまい!"},
{"content": "甘い!"},
{"content": "楽しい!"}
]
]
}
}
在另一方面,由于Query中定义了item字段,因此也可以进行这样的数据提取。
query {
item("DF9B698A-A70E-4DC0-989A-8003A8168E11"): {
name,
revs: {
content
}
shop: {
name
}
}
}
{
"data": {
"name": "甘熟王",
"revs": [
{"content": "うまい!"},
{"content": "甘い!"},
{"content": "楽しい!"}
],
"shop": {
"name": "バナナ屋"
}
}
感觉类似于对内存中对象的处理方式。
可以定义接口
此外,GraphQL的schema中还可以定义接口。与Java或PHP类似,可以按以下方式使用。
interface Node {
id: ID!
}
type Shop implements Node {
id: ID!,
name: String!
items: [Item]
}
type Item implements Node {
id: ID!
name: String!
revs: [Review]
}
:
在实现与Java等类似的接口时,需实现接口中定义的字段。关于具体细节,省略不谈。
GraphQL的本质仅仅是一种查询语言的规范。
GraphQL是一种针对对象数据的查询/操作接口规范,并不是一个名为GraphQL的数据库产品或应用程序服务器。就像SQL不是具体的产品一样,而是用作关系数据库管理系统(RDBMS)等数据库查询语言一样。对于GraphQL处理的数据存储类型没有特殊限制。GraphQL的详细规范可在GraphQL上公开。
不同的编程语言都有各种用于实现GraphQL服务器的库,可以在Code | GraphQL上进行确认。似乎还有一些未提及的库。由于不同的库支持的功能范围不同,它们可能提供了基于GraphQL规范的实现,包括查询的词法解析/语法解析和基于后面提到的模式的验证,以及提供了一些实用工具。然而,对于查询的实现和数据存储方面,基本上是超出了这些库的范围,所以在这方面,实现者需要自行设计/实现。
因此,迄今为止只定义模式,只能期望写出这样的查询并返回这样的数据,实际的实现是必要的。作为实现的入门,建议参考“Getting Started With GraphQL.js | GraphQL.js Tutorial”。
其他
由于一篇文章无法完全涵盖GraphQL的所有特点,所以建议您参考官方网页以了解其他特点和详细信息。
GraphQL介绍|GraphQL
更加实用的用法
在前一章中提到的Shop – Item – Review – User的GraphAPI在现实中是无法使用的。因为我们无法知道与Shop相关联的Item数量,以及与Item相关联的Review数量。在这种情况下,如果尝试解决所有的对象图,可能会消耗网络带宽,迅速耗尽服务器资源。
如果不需要考虑这一点的话,可以将上面的对象图通过REST API表示出来,但我认为这正是一个问题所在。在使用RESTful API的实际情况下,我认为Shop资源的响应中可能会包含与Shop相关的Item资源的id或资源的URI。
GraphQL规范用于中继
所以,我对GraphQL如何解决这个问题很感兴趣。然而,GraphQL的语言规范本身并没有特别涉及这一点。因此,我一直在思考如何处理这个问题,然后我阅读了《GraphQL中继规范 | Relay文档》,顿时恍然大悟。
Relay是由Facebook开发的用于React的数据访问框架,它是GraphQL的创始者。它是基于GraphQL服务器后端构建的。在Relay的文档中,定义了Relay所需的GraphQL服务器的规范。阅读这份文档时,可以发现其中描述了专为Relay定制的GraphQL子集要求。根据这份文档,为了满足Relay的需求,GraphQL需要满足以下要求(意译自原文)。
-
- 为了确保能够准确地重新获取相同的对象,给予对象唯一的标识符。
-
- 为了表示关于集合数据分页的信息,设计了一个称为「连接」的机制。
- 实现了能够预测对象变化的机制。
就仅看这一点来说,我清楚地认为有点搞不明白是关于什么的。所以,在下一个部分我会更详细地讲解一下。不过,由于我自己对突变周围的想法还没有很好地整理出来,因此我只想对1、2的对象识别和集合数据的分页进行解释。
物体的识别
这是由已经存在的ID所担保的。在实现上,GraphAPI处理的数据必须确保有ID。这也涉及到DDD中对于对象同一性的讨论。要实现这个,如果后端是RDB,则需要在表示对象的实体表中设置唯一键;如果有其他方式,则需要生成类似UUID的不重复ID的机制。不过,我认为从概念上来说,这一点并不太难理解。
如何处理集合数据的分页问题。
上述的文件中解释了“连接”的概念,但是我觉得有点难以理解。但是,在阅读完上面的文件后,我在查看Github的GraphQL API时有了理解(很可能,Github的API也是考虑了GraphQL Relay规范进行设计的)。所以,对于那些读了下面仍然不明白的人,我建议您阅读Github的GraphQL API文档。
在Relay GraphQL中,为了实现分页功能,我们使用了两个接口:Connection和Edge。在GraphQL Schema中表示如下:
interface Connection {
pageInfo: PageInfo
edges: [Edge]
}
interface Edge {
cursor: String
node: Node!
}
interface Node {
id: ID!
}
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
如果用稍微概念化的图表来表示这个,大致如下。
Node在对象图中是要表达的概念。如果你读过DDD,我认为你应该很容易理解Relay GraphQL中Node实现的类型,与DDD中的实体相对应。
一方面,如果用系统表达真实世界,实体将会具有数不清的其他实体,而GraphQL系统引入了Edge和Node这样的机制。Edge在Node存储列表中起到了带有编号的盒子的角色。Connection可以被看作是以子列表方式分批提取Node列表的聚光灯机制(或许这个比喻也有点难以理解)。
让我们不再纸上谈兵,实际地去看看GitHub的API。
如果想知道”livesense-inc”这个组织拥有的公开存储库的名称,可以发送以下查询。
我相信你能够理解这种关系,就像上面的概念图一样。
在Organization的repositories字段中,需要指定一个叫做first或last的整数类型参数。这将告诉我们在获取的Connection中包含多少个Edge的信息。此外,虽然上面的截图中没有提到,但也可以指定一个叫做after的参数。如果指定了特定的cursor ID,那么我们就可以获取该cursor之后的n个Edge。对于这种connection的访问接口是通用的,并且都设计成通过Connection和Edge来访问对象集合。
通过以上的改进,我们能够在RESTful API中避免因提取所有嵌套数据而导致数据量巨大的问题,并且能够从对象图的特定节点中,以所需的深度和信息一次性查询到所需的信息。而且,很可能不需要对GraphQL的查询语言规范本身进行特别修改,在该机制范围内,通过创建Edge和Connection这样的概念性开销或类型,我们可以创建非常灵活的API,这让我感到非常激动(微笑)。虽然我觉得在所有对象上实施Connection和Edge可能有些繁琐,但是这个功能似乎可以通过graphql/graphql-relay-js这个GraphQL扩展库的实用工具来处理这些样板代码,我觉得这很棒。
终点
由于这篇文章没有详细讨论突变和实施,我希望能在另一篇文章中总结一下相关内容,等待后续发布。
尽管篇幅有点长,但若您对GraphQL有丝毫兴趣,我将不胜感激(并非我的臆测)。
(Note: The response provided is in Simplified Chinese. If you prefer Traditional Chinese, please let me know.)
追加备注:
我使用React和Apollo尝试了Github GraphQL API,并为GraphQL客户端编写了示例代码和解释。如果您有兴趣,请来瞧一瞧。
这只是关于Github以HTTP为基础提供GraphQL的讨论,并不意味着GraphQL的通信协议一定要是HTTP。根据实际调查,似乎还存在使用Thrift、MessagePack、ProtocolBuffer等RPC协议进行实现的情况。
虽然在Github的GraphQL API中,并没有将Connection和Edge定义为接口的概念,但是它实质上具有接口的特性,所以在这里我们将其描述为接口。