前言
关于对GraphQL的疑问:
- GraphQL 与图形数据库有什么关系?
它们真的没有关系,GraphQL 与诸如 Neo4j 之类的图形数据库没有任何关系。名称中的 “Graph” 是来自于 GraphQL 使用字段与子字段来遍历你的 API 图谱;“QL” 的意思是“查询语言”(query language)。- 我用 REST 用的很开心,为什么我要切换成 GraphQL 呢?
如果你使用 REST 还没有碰上 GraphQL 所解决的那些痛点,那当然是件好事啦!
但是使用 GraphQL 来代替 REST 基本不会对你 app 的用户体验产生任何影响,所以“切换”这件事并不是所谓“生或死”的抉择。话虽如此,我还是建议你如果有机会的话,先在项目里小范围地尝试一下 GraphQL 吧。- 如果我不用 React、Relay 等框架,我能使用 GraphQL 吗?
当然能!因为 GraphQL 仅仅是一个标准,你可以在任何平台、任何框架中使用它,甚至在客户端中也同样能应用它(例如,Apollo 有针对 web、iOS、Angular 等环境的 GraphQL 客户端)。你也可以自己去做一个 GraphQL 服务端。
GraphQL 是 Facebook 做的,但是我不信任 Facebook
再强调一次,GraphQL 只是一个标准,这意味着你可以在不用 Facebook 一行代码的情况下实现 GraphQL。
并且,有 Facebook 的支持对于 GraphQL 生态系统来说是一件好事。关于这块,我相信 GraphQL 的社区足够繁荣,即使 Facebook 停止使用 GraphQL,GraphQL 依然能够茁壮成长。
“让客户端自己请求需要的数据”这整件事情听起来似乎不怎么安全……
你得自己写自己的 resolver,因此在这个层面上是否会出现安全问题完全取决于你。
GraphQL真面目
- 什么是GraphQL?
- GraphQL的特点
- 为什么要用GraphQL?
- GraphQL的使用
- GraphQL客户端
- 一些关于Graphql 的重要概念解释
- Granphql Schema
- Granphql 类型系统(Type System)
- 对象类型和字段(Object Types and Fields)
- 标量类型(Scalar Types)
- 集合 (List)
- 空(null)、非空 (Non-Null)
- 参数(Arguments)
- Resolver
- 总结
什么是GraphQL?
官方解释:GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(
类型系统
由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑,GraphQL 可以运行在任何后端框架
或者编程语言
之上。
GraphQL的特点
GraphQL 是一种描述请求数据方法的语法,通常用于客户端从服务端加载数据。GraphQL 有以下三个主要特征:
- 它允许客户端指定具体所需的数据。
- 它让从多个数据源汇总取数据变得更简单。
- 它使用了类型系统来描述数据。
为什么要用GraphQL?
GraphQL可以理解为是一个基于新的API标准,或者说基于对RESTful的封装的一种API查询语言。在过去的很多年里,RESTful被当做了API设计的一种标准(这里说RESTfu是一种设计标准其实不准确,说它是一种软件架构风格、设计风格更好,它只是提供了一组设计原则和约束条件。暂且这么说了。),但是在客户需求快速变化的今天,RESTful API显得有些死板僵化。而GraphQL的推出就是为了针对性的解决客户端数据访问的灵活性和高效。
怎么理解RESTful API 在适应当今复杂需求的时显露出来的僵化问题?或者说相比较而言Graphql API 的优势是什么呢?
在开发中RESTful API接口返回的数据格式、数据类型都是后端预先定义好的,如果返回的数据格式并不是调用者(前端)理想型,前端一般会通过以下两种方式来解决:
- 和后端沟通,改接口(更改数据源)
- 前端自己做适配工作(处理数据源)
一般如果是小型项目,或者说对应的是单前端需求,改后端接口比较好商量,对这个项目的有效运行影响不大。但是如果是大项目,譬如,一个后端API对应的是三接口,什么意思呢,就是一个后端API需要同时满足web、Android、IOS前端的不同的数据需求,这种情况下为了某一方需求要求改后端API,明显会顾此失彼,不现实。所以一般这种情况就需要前端自己做适配来满足自己的需求。
正是为了适应当今愈加复杂的需求环境,Facebook推出了Graphql API,一个可以帮助前后端解耦的API查询语言,让接口的返回值从静态变为动态,即调用者(前端)来声明接口返回什么数据。
GraphQL的使用
GraphQL工作流程:
GraphQL客户端
- GraphQL PalyGround
- relay
Relay 是 Facebook 的 GraphQL 工具。我还没用过它,但是我听说它主要是为了 Facebook 自己的需求量身定做的,可能对大多数的用户来说不是那么人性化。
在这个领域的最新参赛者是 Apollo,它正在迅速发展。典型的 Apollo 客户端技术栈由以下两部分组成:
- Apollo-client,它能让你在浏览器中运行 GraphQL 查询,并存储数据。(它还有自己的开发者插件)。
- 与你用的前端框架的连接件(例如 React-Apollo、Angular-Apollo 等)。
一些关于Graphql 的重要概念解释
Granphql Schema
每一个 GraphQL 服务都会定义一套类型,用以描述你可能从那个服务查询到的数据。每当查询到来,服务器就会根据 schema 验证并执行查询。那么这里的说的schema什么?这关乎我们理解Granphql 类型系统,所以在说Granphql 类型系统之前,我们的先弄懂GraphQL Schema。
与XML Schema 的概念类似,Schema由服务端来定义,用于定义API接口,并依靠Schema来生成文档以及对客户端请求进行校验。Schema只是一个概念,它是由各种数据类型及其字段组成,而每个类型的每个字段都有相应的函数来返回数据。如下面这个,每一个garphqls文件就是一个Schema:
type Query {
#店铺对象
shops:Shops!
}
# 店铺对象
type Shops{
# 店铺id
id:Int!
# 平台
platform:String
# 昵称
platformNick:String
# 平台id
platformId:String
# 店铺名称
shopName:String
# 日期
date:String
}
# 添加店铺
input addShopInput {
shopId: Int!
shopName: String!
}
# 更新店铺
input updateShopInput {
shopId: Int!
shopName: String!
}
上面定义了两个类型,Shops和Query:Shops有几个字段分别是id、platform、platformNick、platformId、shopName、date;Query是个特殊的类型,是查询入口,所有要查询的都放着里面。
Granphql 类型系统(Type System)
GraphQL作为一种应用层的查询语言,它有自己的数据类型,用来描述数据(换句话说:用它自己的数据类型来定义你想要查询的对象及对象的属性,类似Java定义属性或对象的数据类型:String name、JSONObject data。Graphql定义查询对象和对象属性有标量类型、集合类型、Object类型、枚举等,甚至有接口类型,这些构成了Graphql的Type System。
GraphQL 服务可以用任何语言来编写运行,因为它并不依赖于任何编程语言的句法来与 GraphQL schema 沟通,它定义了自己的语言:GraphQL schema language 。下面让我们一起看下:
对象类型和字段(Object Types and Fields)
一个 GraphQL schema 中的最基本的组件是对象类型,它表示你可以从服务器上获取到什么类型的对象,以及这个对象的所有属性字段(如果你需要它所有字段的话)。使用 GraphQL schema language,可以如下表示:
#国家对象
type Country{
#国家名称
name: String
#省份
province: String
}
- Country是一个 GraphQL 对象类型,同Java对象一样,它也会拥有一些字段。schema 中的大多数类型都会是对象类型。
- name 和 province是 Country对象类型上的字段。当你查询Country对象时,就会获得有且仅有 name 和 province字段。
- String 是内置的标量类型,标量类型下面详述。
- #号是GraphQL schema中用来注释代码的.
标量类型(Scalar Types)
从上面说到的对象类型可以知道,一个对象类型同java对象一样,有自己的对象名和属性字段,它的属性字段也需要Graphql的类型系统给出具体类型来注释,以便同服务器的传递过来的不同类型的数据不起冲突。所以Graphql的类型系统便定义了标量类型,它是解析到单个标量对象的类型,无法在查询中对它进行次级选择,标量类型是不能包含子字段(通俗来讲,你在上面描述文件中,按住ctrl键,鼠标点击String,点不进去了,但是你点击第一个Dwxx却可以点进去,并定位到下面的type Dwxx)。
目前 GraphQL 自带的标量类型有如下几个(注意首字母):
- Int:有符号 32 位整数。
- Float:有符号双精度浮点值。
- String:UTF‐8 字符序列。
- Boolean:true 或者 false。
- ID:常用于获取数据的唯一标志,或缓存的键值,它也会被序列化为String,但可读性差
除此之外,我们也可以自定以标量类型来满足我们的实际开发需求,比如,我可以定义一个名为Map的标量。
/**
* 自定义标量类型 - Long
*
*/
@Component
public class MapScalar extends GraphQLScalarType {
public MapScalar() {
super("Map", "Built-in Long as java.util.Map", new Coercing() {
@Override
public Map serialize(Object input) throws CoercingSerializeException {
Map<String, Object> map = new HashMap<>();
if(input instanceof Map){
Set set = ((Map) input).keySet();
for (Object o : set) {
Object value = ((Map) input).get(o);
map.put(o.toString(),value);
}
}
return map;
}
@Override
public Map parseValue(Object input) throws CoercingParseValueException {
return null;
}
@Override
public Map parseLiteral(Object input) throws CoercingParseLiteralException {
return null;
}
});
}
}
# 引用自定义标量Map
scalar Map
type ResultData {
# 唯一id,无实质意义
serialVersionUID:ID!
# 请求状态码
code: Int
# 请求信息
msg: String
# 请求数据 -各类型数据
data: Map
}
集合 (List)
在GraphQL规范中可以使用一个类型修饰符方括号:[] 来标记一个类型为 List,这有点像java中定义数组。它表示这个字段会返回这个类型的数组(集合)。
例如下面这个例子,一个Country有很多个province,province就表示为一个集合或者说数组:
#国家对象
type Country{
#国家名称
name: String
#省份
province: [String]
}
空(null)、非空 (Non-Null)
在GraphQL 规范中,通过在类型名后面添加一个叹号: ! 来将其标注为非空。这样后台服务器就必须要对这个字段返回一个非空的值。如果后台服务器返回了一个null给这个字段,GraphQL 就发生错误,同时客户端也会受到这个错误信息。
例:
#国家对象
type Country{
#国家名称
name: String!
#省份
province: [String!]
#市
city:[String]!
#乡镇
town:[String!]!
}
- String! 后面加的这个英文叹号表示name这个字段是非空的,当你查询这个字段时Graphql必须要给你返回一个非null的值。
- [String!] 表示province的返回值是一个非空字符串的数组(集合)。即数组本身可以为空,但是其不能有任何空值成员,详细参照下面:
province: null // 有效
province: [] // 有效
province: ['a', 'b'] // 有效
province: ['a', null, 'b'] // 错误
参数(Arguments)
实际开发中,我们可能需要前端提交查询请求的同时,传递参数给我们供我们后台服务器拿去执行查询。
举个例子,现有这样一个需求背景:前端需要查询单位信息数据,后台提供的查询接口,支持前台输入单位代码(dwdm),机构名(jgm)等参数来精确查询,其中单位代码是比输入项,机构名为选填。那么graphql的描述文件(或者说schema )可以这样写:
type query{
#查询单位信息
dwxx(
#单位代码,必填
dwdm:String!
#单位名称
dwmc:String
):Dwxx
}
type Dwxx{
id:Int
..
这里是要返回的单位信息对象类型的一些属性字段
}
Resolver
如果你仅在Schema中的Query{ }查询接口中声明了若干子query函数和定义它的返回值类型,那么此刻你只完成了一半的工作,因为你还需要提供相关Query所返回数据的后台逻辑。所以为了能够使GraphQL正常工作,你还需要再了解一个核心概念——Resolver(解析函数)。
GraphQL中官方文档中对于每个子Query和与之对应的Resolver有明确的定义规范,以确保GraphQL能把Schema中每个子query同它们的Resolver一一对应起来,最终实现后端逻辑处理数据后丢给Graphql,Graphql再返回数据给前端。
Granphql官方文档中,Resolver命名规范如下(注意红色部分就是方法名命名格式,任选其一就可):
-
<fieldname>
(dataRepositoryClassInstance, *fieldArgs [,
DataFetchingEnvironment]) -
is<Fieldname>
(dataRepositoryClassInstance, *fieldArgs [,
DataFetchingEnvironment]) -
get<Fieldname>
(dataRepositoryClassInstance, *fieldArgs [,
DataFetchingEnvironment]) -
getField<Fieldname>
(dataRepositoryClassInstance, *fieldArgs [,
DataFetchingEnvironment])
例:
# 查询相关接口
type Query {
#### shops_machine shops
# 根据主键Id查询店铺分配信息 - false
getShops(id:Int!): Shops
# 据compsId查找分配信息 - false
getComps(id:Int!): Comps
#### shops/shops2 pddOrder
# 获取订单列表数据
getPddOrderList(pddOrderCondition:PddOrderParams!): ResultData
}
# 变更相关接口
type Mutation {
#### shops/shops2 shop
# 添加店铺 - false
saveShop(shop: addShopInput!): Boolean
# 更新店铺 - false
updateShop(shop: updateShopInput!): Boolean
}
这Query中的子query中Resolver的名字可以为shop、isShop、getShop、getFieldShop,一般我会直接选第三种方式。
注:
Query表示查询入口;Mutation表示修改入口。即查询用Query,增删改用Mutation。
总结
当你刚开始接触 GraphQL 可能会觉得它非常复杂,因为它横跨了现代软件开发的众多领域。但是,如果你稍微花点时间去明白它的原理,我认为你可以找到它很多的可取之处。
所以不管你最后会不会用上它,我相信多了解了解 GraphQL 是值得的。越来越多的公司与框架开始接受它,过几年它可能会成为 web 开发的又一个重要组成部分。