GraphQL
GraphQL 是一种开放源码的数据查询和操作语言,以及一个用于使用现有数据完成查询的运行时。
GraphQL于2012年由Facebook内部开发,2015年公开发布。
它提供了一种更高效、更强大和更灵活的替代REST和特别web服务体系结构的方法。允许客户机定义所需数据的结构,并且从服务器返回的数据结构完全相同,因此可以防止返回过多的数据。
GraphQL支持读取、写入(修改)和订阅数据更改(实时更新).
GraphQL主要客户端包括Apollo客户端和Relay。
GraphQL服务器可用于多种语言,包括Haskell、JavaScript、Python、Ruby、Java、c#、Scala、Go、Elixir、Erlang、PHP、R和Clojure。
2018年2月9日,GraphQL模式定义语言(SDL)成为规范的一部分
优势
1、为了得到视图所需的数据,需要进行多轮的网络调用:借助 GraphQL,要获取所有的初始化数据,我们仅需一次到服务器的网络调用。要在 REST API 中达到相同的目的,我们需要引入非结构化的参数和条件,这是很难管理和扩展的。
2、客户端对服务端的依赖:借助 GraphQL,客户端会使用一种请求语言,该语言。 1)消除了服务器端硬编码数据形式或数量大小的必要性; 2)将客户端与服务端解耦。这意味着我们能够独立于服务器端维护和改善客户端。
3、糟糕的前端开发体验:借助 GraphQL,开发人员只需使用一种声明式的语言表达用户的界面数据需求即可。他们所描述的是需要什么数据,而不是如何得到这些数据。在 GraphQL 中,UI 所需的数据以及开发人员描述数据的方式之间存在紧密的联系。
REST API 的缺陷
REST API 最大的问题在于其多端点的特质。
这需要客户端进行多轮请求才能获取到想要的数据。
REST API 通常是端点的集合,其中每个端点代表了一个资源。
所以,当客户端需要来自多个资源的数据时,就需要针对 REST API 发起多轮请求,这样才能将客户端所需的数据组合完整。
在 REST API 中,没有客户端请求语言。客户端对服务端返回的数据没有控制权。
在这方面,没有语言能够帮助它们实现这一点。更精确地说,客户端可用的语言非常有限。
- 实例
例如,用来实现读取(READ)的 REST API 一般不外乎如下两种形式:
GET /ResouceName:获取指定资源的所有记录的列表,或者
GET /ResourceName/ResourceID:根据 ID 获取单条记录。
举例来说,客户端无法指定该选择记录中的哪个字段。
这些信息位于 REST API 服务本身之中,不管客户端实际需要哪些字段,REST API 服务始终都会返回所有的字段。GraphQL 对该问题的描述术语是过度加载(over-fetching)不需要的信息。不管是对于客户端还是对于服务器端,这都是网络和内存资源的一种浪费。
REST API 的另外一个大问题是版本化。
如果你需要支持多版本的话,通常意味着要有多个端点。在使用和维护这些端点的时候,这通常会导致更多的问题,而这也可能是服务端出现代码重复的原因所在。 上文所述的 REST API 的问题恰好是 GraphQL 所要致力解决的。
上面所述的这些肯定不是 REST API 的所有问题,我也不想过多讨论 REST API 是什么,不是什么。我主要讲的是基于资源的 HTTP 端点 API。这些 API 最终都会变成常规 REST 端点和自定义专门端点的混合品,其中自定义的专门端点大多都是因为性能的原因而制作的。
在这种情况下,GraphQL 能够提供好得多的方案。
GraphQL 的魔力如何实现的?
设计理念
在 GraphQL 背后有着很多理念和设计决策,但是最为重要的包括:
- GraphQL 模式是强类型的模式。
要创建 GraphQL 模式,我们需要按照类型来定义字段。这些类型可以是原始类型,也可以是自定义类型,模式中的任何内容都需要一个类型。这种丰富的类型系统允许实现丰富的特性,比如具备内省功能的 API,以及为客户端和服务端构建强大的工具;
- GraphQL 将数据以 Graph 的形式来进行表示,而数据很自然的表现形式就是图。
如果想要表示任意的数据,那正确的结构就是图。GraphQL 运行时允许我们以图 API 的方式来表示数据,该 API 能够匹配数据的自然图形形状;
- GraphQL 具有一个声明式的特质来表示数据需求。
GraphQL 为客户端提供了一种声明式的语言,允许它们描述其数据需求。这种声明式的特质围绕 GraphQL 语言创建了心智模型,这与我们使用自然语言思考数据需求的方式非常接近,从而使得 GraphQL API 要比其他替代方案容易得多。
问题的解决
- 客户端的可控性
为了解决多轮网络调用的问题,GraphQL 将响应服务器变成了只有一个端点。 从根本上来讲,GraphQL 将自定义端点的思想发挥到了极致,将整个服务器变成了一个自定义的端点,使其能够应对所有的数据请求。
与这个单端点概念相关的另一个重要理念是富客户端请求语言(rich client request language),这是使用自定义端点所需要的。如果没有客户端请求语言的话,单端点是没有什么用处的。它需要有一种语言来处理自定义的请求并为该请求响应数据。
具备客户端请求语言就意味着客户端将会是可控的。
客户端能够确切地请求它们想要的内容,服务器端则能够确切地给出客户端想要的东西。这解决了过度加载的问题。
- 版本问题
在版本化方面,GraphQL 有一种非常有趣的做法,能够彻底避免版本化的问题。从根本上来讲,我们可以添加新的字段,而不必移除旧的字段,因为我们有一个图,从而可以通过添加节点来灵活地扩展这个图。
所以,我们可以继续保留旧 API 的路径,并引入新的 API,而不必将其标记为新版本。API 只是不断增长而已。
这对于移动端尤为重要,因为我们无法控制它们使用哪个版本的 API。
一旦安装之后,移动应用可能会多年一直使用相同版本的旧 API。
在 Web 端,我们能够很容易地控制 API 的版本,我们只需推送并使用新的代码即可。对移动应用来说,这样做就有些困难了。
实例对比
需求
假设我们要构建的第一个 UI 界面很简单:显示每个《星球大战》人物信息的视图。例如,Darth Vader 以及他在哪些电影中出现过。这个视图将会展现人物的姓名、出生年份、星球名称以及他们所出现的电影的名字。
- json 格式
{
"data": {
"person": {
"name": "Darth Vader",
"birthYear": "41.9BBY",
"planet": {
"name": "Tatooine"
},
"films": [
{ "title": "A New Hope" },
{ "title": "The Empire Strikes Back" },
{ "title": "Return of the Jedi" },
{ "title": "Revenge of the Sith" }
]
}
}
}
REST API
现在,我们看一下如何使用 RESTful API 请求该数据。
- 人的信息
我们需要一个人的信息,假设我们知道人员的 ID,暴露该信息的 RESTful API 预期将会是这样的:
GET - /people/{id}
这个请求将会为我们提供该人员的姓名、生日和其他信息。一个好的 RESTful API 还会给我们提供该人员的星球 ID 以及这个人员所出现的所有电影的 ID 数组。
{
"name": "Darth Vader",
"birthYear": "41.9BBY",
"planetId": 1
"filmIds": [1, 2, 3, 6],
// *** 其他我们并不需要的信息 ***
}
- 星球信息
为了读取星球的名称,我们需要调用:
GET - /planets/1
- 电影
随后,为了读取电影的名称,我们还要调用:
GET - /films/1
GET - /films/2
GET - /films/3
GET - /films/6
从服务器端得到这六个响应之后,我们就可以将它们组合起来以满足视图的数据需求。 为了满足一个简单 UI 的数据需求,我们发起了六轮请求,除此之外,我们在这里的方式是命令式的。
我们需要给出如何获取数据以及如何处理数据使其满足视图需求的指令。
我们可能还会有更好的实现方式,让视图编写起来更加容易。
例如,如果 API 服务器的实现能够嵌套资源并理解人员和电影之间的关联关系,那么我们通过该 API 来读取电影数据:
GET - /people/{id}/films
但是,纯粹的 RESTful API 可能并不会实现这些,我们需要请求后端工程师为我们创建这个自定义的端点。
这就是 RESTful API 进行扩展的现实:我们只能添加自定义端点来有效满足不断增长的客户端需求。管理这样的自定义端点是非常困难的。
GraphQL
GraphQL 在服务端拥抱了自定义端点的理念,并将其发挥到了极致。服务器只有一个端点,至于通道则无关紧要。
如果你通过 HTTP 来实现的话,HTTP 方法也是无关紧要的。
我们假设有一个通过 HTTP 暴露的 GraphQL 端点,其地址为 /graphql:
因为想要通过一轮请求就将数据获取到,所以需要有一种方式来向服务器表达完整的数据需求。
我们通过一个 GraphQL 查询来实现这一点:
GET or POST - /graphql?query={...}
GraphQL 查询只是一个字符串,但是它需要包含我们所需数据的所有片段。此时,声明式的方式就能发挥作用了。 在中文中,会这样描述我们的数据需求:我们需要一个人员的姓名、出生年份、星球的名字以及所有相关电影的名称。
在 GraphQL 中,这会翻译为:
{
person(ID: ...) {
name,
birthYear,
planet {
name
},
films {
title
}
}
}
再次阅读一下使用中文表达的需求,然后将其与 GraphQL 查询进行对比。你会发现,它们非常接近。现在,对比一下这个 GraphQL 查询和我们开始时所见到的原始 JSON 数据。
GraphQL 查询与 JSON 数据的格式完全相同,唯一的差异在于“值(value)”部分。
如果我们将其想象为问题和答案的关系的话,所提出的问题就是将答案语句刨除了答案值。
代价
完美的解决方案只可能出现在童话之中。GraphQL 带来了灵活性,同时也有一些值得关注的地方和问题。
拒绝服务攻击
GraphQL 所带来的一个非常重要的风险就是资源耗尽攻击(即拒绝服务攻击)。
GraphQL 服务器可以通过过于复杂的查询来进行攻击,这种查询将会消耗尽服务器的所有资源。
它也非常容易查询深层的嵌套关联关系(用户 ->好友 ->好友),或者使用字段别名多次查询相同的字段。
资源耗尽攻击并不是 GraphQL 特有的,但是在使用 GraphQL 的时候,我们必须格外小心。
N+1 SQL
关于 GraphQL,我们最需要关注的问题可能就是所谓的 N+1 SQL 查询。
GraphQL 的查询字段被设计为独立的函数,在数据库中为这些字段解析获取数据可能会导致每个字段都需要一个新的数据库请求。
对于简单的 RESTful API 端点,可以通过增强的结构化 SQL 查询来分析、检测和解决 N+1 查询问题。
GraphQL 动态解析字段,因此并没有那么简单。幸好,Facebook 正在通过 DataLoader 方案来解决这个问题。
- DataLoader
顾名思义,DataLoader 是一个工具,我们可以借助它从数据库中读取数据,并将其提供给 GraphQL 解析函数使用。
我们可以使用 DataLoader 读取数据,避免直接使用 SQL 查询从数据库中进行查询,DataLoader 将会作为我们的代理,减少实际发往数据库中的 SQL 查询。
DataLoader 组合使用批处理和缓存来实现这一点。如果相同的客户端请求需要向数据库查询许多内容的话,DataLoader 能够合并这些问题,并从数据库中批量加载问题的答案。
DataLoader 还会对答案进行缓存,后续的问题如果请求相同的资源的话,就可以使用缓存了。
参考资料
https://en.wikipedia.org/wiki/GraphQL
https://mp.weixin.qq.com/s/aubiT7n6eLqaAsbeec2aHQ
- N+1 Query
https://stackoverflow.com/questions/97197/what-is-the-n1-select-query-issue
https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/
https://blog.csdn.net/xtayhicbladwin/article/details/4739852
- graph sql