前言

关于对GraphQL的疑问:

graphx数据结构 spark graphsql_字段

  • 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工作流程:

graphx数据结构 spark graphsql_编程语言_02

GraphQL客户端

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:

graphx数据结构 spark graphsql_graphx数据结构 spark_03

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 开发的又一个重要组成部分。