前言
GraphQL已经被越来越多的开源项目作为业务接口的开发规范,而在此之前流行的是restful接口。那么GraphQL是什么?GraphQL相比restful有哪些好处?本文带你初窥GraphQL的门道,看看你们的业务是否适合使用GraphQL。
GraphQL是什么
GraphQL = Graph + QL (query language)
GraphQL是一种图表化(graph)的查询语言(query language),用于定义API的查询语法。GraphQL只定义api查询语法和数据规范,没有限制前端和后端的类型,也没有限制存储层的实现。
可以类比SQL,SQL仅定义了一套数据操作与查询语言的规范,Mysql,oracle等各大数据库开放了符合SQL标准的接口,当然你可以自己开发一个使用SQL查询的业务接口。
官网解释:
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data
GraphQL既是一种用于API的查询语言也是一个满足你数据查询的运行时。
GraphQL的组成结构
1. schema
GraphQL通过schema定义api和数据结构,schema通常以文件形式存在,包含一系列的对象定义。对象结构为类json格式的字符串。
根据总结,对象定义的格式遵守以下规则:
${对象类型} ${对象名称} {
#字段
${字段名称}: ${字段类型}
#字段类型为数组
${字段名称}: [${字段类型}]
#字段类型为非空对象
${字段名称}: ${字段类型}!
#带参数的字段
${字段名称} (${对象名称} : ${对象类型}): ${字段类型}
}
GraphQL定义了一些标量类型,可作为对象的字段定义。标量类型有:Int、Float、String、Boolean、ID。
其中ID类型是GraphQL定义的唯一标识符,其本质上也是字符串。
GraphQL支持枚举类型enum,枚举类型定义示意:
enum Gender {
male
female
}
GraphQL的对象类型主要分为两类,数据对象和接口对象,分别用于定义接口的输入输出和接口实体。
数据对象
- type
自定义名称的类型(关键字除外),用于定义接口查询的返回对象类型以及内联对象类型。
type Man {
name: String
age: Int
gender: Gender
}
- interface
接口类型interface,和type对象定义基本一致。
interface Human {
name: String
age: Int
gender: Gender
}
对象可以通过implements申明实现了某个接口对象,实现对象必须包含接口对象的所有字段,方便对相同业务属性进行抽象。
type Man implements Human {
name: String
age: Int
gender: Gender
}
- input
输入类型input本质上也是一个type类型,是作为Mutation接口的输入参数。换言之,想要定义一个修改接口(add,update,delete)的输入参数对象,就必须定义一个input输入类型。
input ManInput {
name: String
age: Int
gender: Gender
}
值得一提的是,在input对象中的对象也必须是input类型,这可能是一个比较局限的设计。
接口对象
接口对象分为数据查询(select)接口和数据操作(add,update,delete)接口,分别用Query关键字和Mutation关键字来定义。下面举两个例子:
定义一个名为search的查询接口,返回对象为Human类型:
type Query {
search(name: String): Human
}
定义一个名为add的数据插入方法,方法参数为Human类型,返回对象也为Human类型:
type Mutation {
add(human: Human): Human
}
Mutation类型不对数据的操作含义进行区分,不像SQL一样,有ADD、UPDATE、DELETE,统一使用Mutation进行定义,具体的操作含义在接口的实现中完成。
2. query
通过schema对api进行定义之后,后续发起的graphql api请求都必须严格遵守这个定义。特别的,在graphql的api请求中可以指定返回对象的部分而非全部的字段,从而减少一些不必要的数据传输。
GraphQL的api请求只有Query和Mutation两种,下面简单介绍下Query和Mutation的查询语法。
简单query语句
//调用search接口,传入一个名为name的字符串参数,期望的返回类型是Human对象中的age字段
query {
search(name: "马云"): {
age
}
}
带参数的graphql api请求附带一个专门用于描述请求参数json数据,json的key和api参数名称对应。
带对象参数的query语句
//调用search接口,传入一个名为human的Human对象参数,期望的返回值是Human对象中的age字段
query queryHuman($human: Human) {
search(human: $human): {
age
}
}
//graphql的参数,以json格式给出,对应类型Human
graphql variables
{
"human": {
"name": "马云"
}
}
带对象参数的Mutation语句
//调用add接口,传入一个名为human的Human对象参数,期望的返回值是human对象的三个字段
mutation addHuman($human: Human) {
add(human: $human): {
name
age
gender
}
}
//graphql的参数,以json格式给出,对象的结构和schema定义必须一致
graphql variables
{
"human": {
"name": "马云",
"age": 50,
"gender": "male"
}
}
3. datafetchers
datafetcher其实是graphql-java中的一个组件,其作用是查询和操作业务数据,并提供api的返回值。datafetcher会通过显式的申明和query绑定起来,从而实现api和数据层的交互。
任何一门语言实现graphql都需要类似datafetcher这样一个组件来实现api和数据层的交互。
GraphQL能带来什么好处?
使用GraphQL能带来什么好处呢?我总结主要有以下几点:
- 强一致性
graphql的协议规定了api和schema的定义必须保持一致,这种文档先行的开发模式,使得项目的架构更加清晰,并且使得接口本身就多了一层校验。
- 减少数据冗余
graphql的api在请求时可以指定返回的字段,返回的字段和预期是一致的。这一特性可以让接口在不同的场景下返回必需的字段,减少部分网络开销,并且使得接口的通用性更高。
- 自文档性
得益于graphql的强一致性,使得graphql的schema本身就可以作为api接口文档使用。配合graphql的一些第三方库,可以做到api文档的在线调试和可视化展示,变成真正的图化(graph)查询语言(query language)。
graphiql插件的在线调试api功能
graphql voyager的图形化api查看
GraphQL有什么缺点?
来说说我认为graphql的主要缺点吧:
- 简单问题复杂化
为了实现graphql充分的规范性,也加重了一些开发成本(虽然已经有第三方库可以做到快速开发),任何一个api在开发时都有明确的两步走:1.在schema定义接口和对象 2.实现datafecher,哪怕是一个最简单的get请求。
- 不方便统一控制
graphql不像restful接口,有清晰的层级划分(/a/b/c的结构),graphql的api只能使用接口名称进行区分,接口名称不像层级结构那么容易管理。通常在restful结构下,我们可以通过拦截器对符合某一规则的接口路径进行统一处理(比如对/restful/*的接口进行身份认证),而graphql的请求路径是在同一个url下的,这种处理逻辑就比较难以实现了。
GraphQL框架支持
- graphql-js
graphql官方的js实现,可以快速的搭建js端的graphql服务器,和进行graphql请求。
官方文档地址:
https://graphql.cn/graphql-js/
- graphql-java
graphql官方的java实现,前文也介绍过了,是基于datafetcher的架构,代码比较容易理解,缺点是开发量较大。
官方文档地址:
https://www.graphql-java.com/documentation/v16/
使用graphql java开发graphql接口的demo
public static void main(String[] args) {
//1. 定义schema,这里是字符串直接给出了schema
String schema = "type Query{hello: String}";
SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema);
//2. 定义datafetcher,并和query进行绑定;这里的datafetcher会返回一个固定值world
RuntimeWiring runtimeWiring = newRuntimeWiring()
.type("Query", builder ->
builder.dataFetcher("hello", new StaticDataFetcher("world")))
.build();
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
GraphQL build = GraphQL.newGraphQL(graphQLSchema).build();
//3. 查询时指定查询名hello,不带任何参数,也不指定返回值
ExecutionResult executionResult = build.execute("{hello}");
System.out.println(executionResult.getData().toString());
// Prints: {hello=world}
}
- graphql-kick-start
第三方实现的一个开源graphql java库,优点是能快速接入,配合springboot开发,几乎不需要写代码,并且集成了很多方便的插件如 graphiql,graphql voyager,可以实现真正的图化查询。
官方github地址:
https://github.com/graphql-java-kickstart/graphql-spring-boot
使用graphql-kick-start的graphql servlet插件快速开发graphql接口的demo
#启用 graphql servlet
graphql.servlet.enabled=true
#graphql servlet 使用的url
graphql.servlet.mapping=/graphql
#schema文件路径
graphql.tools.schema-location-pattern=person.schema
person.schema
type Query {
personByName(name: String): Person
}
type Person {
id: Int
name: String
}
//GraphQLQueryResolver的实现类中的方法会自动和schema中的query绑定
@Component
public class PostQuery implements GraphQLQueryResolver {
public Person personByName(String name) {
PersonEntity personEntity = personMapper.findByName(name);
return new Person(personEntity);
}
}
GraphQL的用途
基于graphql的种种特性,以及graphql的库支持的程度来看(似乎官方也认定graphql是用于前后端接口开发的,并未提供除了js之外的客户端实现),目前而言,graphql似乎只适用于前后端之间的接口开发。所幸的是,graphql和restful并不冲突,前端接口可以直接剥离出来,使用graphql规范重新实现,且不影响原有接口。
综上所述,graphql主要用于实现前后端之间的接口,且有graphql本身的强一致性带来了很多规范上的优势。