作者: 一字马胡
转载标志 【2017-11-03】
更新日志
日期 | 更新内容 | 备注 |
---|---|---|
2017-11-03 | 新建文章 | 初版 |
初识GraphQL
GraphQL是一种强大的DSQL,是由Facebook开源的一种用于提供数据查询服务的抽象框架,在服务端API开发中,很多时候定义一个接口返回的数据相对固定的,如果想要获取更多的信息,或者仅需要某个接口的某个信息的时候,基于restful API的接口就显得不那么灵活了,对于这些需求,服务端要么再定义一个新的接口,返回合适的数据,要么客户端就得通过一个庞大的接口来获取一小部分信息,GraphQL的出现就是为了解决这些问题的,GraphQL并不是一门具体的语言实现的某种框架,它是一系列协议文档组成的项目,GraphQL是和语言无关的,而且到现在为止已经有很多语言的实现版本,可以在awesome-graphql看到哪些语言实现了GraphQL,如果想要了解具体的GraphQL定义,可以参考graphql。本文以及本GraphQL系列将只关心Java版本的GraphQL实现,具体的Java版本的GraphQL可以参考graphql-java。下面是官方对GraphQL的描述,很简洁,但是很直观:
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.
下面的图片展示了GraphQL的工作模型:
从这张图片可以看出,GraphQL的位置处于Client和DataSource之间,可以把这一层理解为服务端的API层,所谓API层,就是聚合多个数据源,进行一些业务逻辑的处理,然后提供一些接口给Client调用。而GraphQL就工作在这一层,它相当于是对DataSource的一层抽象,它可以承接Client的请求,然后根据GraphQL的执行引擎来从DataSource获取数据,然后进行处理之后返回json结果给Client,这和Restful的模式没有什么差别,但是GraphQL的强大之处在于GraphQL类似于MySql,Client发送的请求类似于Sql语句,这些Sql语句经过GraphQL解析执行之后返回具体的数据,所以GraphQL具有很好的动态性,Client可以根据不同的需求来使用不同的Sql语句来请求服务端,而GraphQL会解析这些Sql,并且精准的返回结果。这就完美的解决了文章开头提到的难题。使用GraphQL来做服务端API层的开发无疑会减轻服务端开发工程师的很多压力,而且对于Client来说也是很友好的,因为Client不需要想请求Restful接口一样只能获取相对固定的数据,Client可以根据自己的需求使用不同的查询语句来请求GraphQL,使用GraphQL会减少很多冗余的数据传输,并且可以减少很多服务端API层的接口开发工作,API层只需要开发GraphQL服务端,然后告诉Client这些数据的组织结构,然后Client就可以组装出合适的查询语句来请求数据。使用GraphQL进一步将前后端分离(Restful使得前后端分离),后端开发和前端开发可以各自进行,使用GraphQL很多时候服务端是在丰富可以提供的数据,或者优化聚合DataSource来提高响应速度。使用GraphQL还有很多优点,可以研究GraphQL并且使用GraphQL来开发服务端API来体验。本文剩下的内容将基于GraphQL-Java和Spring-boot来实现一个简单的应用,以此来说明使用GraphQL的方法以及使用GraphQL的优势。
需要补充的一点是,上面提到了GraphQL查询语句(上文使用了Sql代替,但不是Sql),这是一种类似于json的结构化数据,可以很轻易的理解它的本意,这也是GraphQL的一个优点,它的查询语句对工程师是很友好的。下文会分析到。
GraphQL 实战
本GraphQL系列的文章基于Java语言以及GraphQL-Java来分析,这一点注意一下。本文的GraphQL示例使用Spring-boot来开发,使用的IDE为idea 17,强烈建议Javaer使用IDEA来开发,可以明显提高开发效率。
为了可以快速上手,下面展示了本文使用的示例的代码结构:
可以根据各个包名来理解这个包管理的类,比如service管理的是一系列service,而view包下是一些需要返回给Client的渲染View。关于如何新建一个Spring-boot项目的过程不再本文的叙述范围之内(唯一说明的一点是,需要Web模块支持),下面根据一些关键步骤来引导如何实现一个GraphQL demo。
创建Model类
这一步很简单,将你需要创建的Model类放到model包下,比如本文的示例想要实现的一个场景是,有一些作者,每个作者可能写了多篇文章,每篇文章都只有一个作者,而每篇文章下面可能没有评论,或者有评论,评论的数量不限,下面是几个关键的类信息:
public class AuthorModel {
private int authorId; // the author id
private int authorAge; // the age
private int authorLevel; // the level
private String authorAddr; // the address
private List<Integer> friends; // the friends of the author
}
public class ContentModel {
private int contentId; // the content id
private int authorId; // the author id
private int commentSize; // the comment size of this content
private String text; // the text
private List<Integer> commentIds; // the Comment id list
}
public class CommentModel {
private int commentId; // the comment id
private int authorId; // the author of this comment
private int ofContentId; // the content id
private String content; // the content of this comment
}
为了实验GraphQL的复杂查询,下面是两个增强类,分别是对AuthorModel类和ContentModel类的增强,可以看到增强之后的类更符合我们的想法:
public class CompletableAuthorModel extends AuthorModel{
private List<AuthorModel> friendsCompletableInfo;
private List<CompletableContentModel> contentModelList;
}
public class CompletableContentModel extends ContentModel{
private List<CommentModel> commentModelList; // the comment info list of this content
}
本文展示的所有代码都可以在github上找到源码,所以本文就不完整的展示所有代码了。
Mock数据
为了测试GraphQL,你需要有一些数据,本文为了快速测试GraphQL,所以Mock的数据比较简单,没有和数据库交互,其实在真实的服务端API层开发中,很多时候是不需要和数据库交互的,更多的是使用RPC来从一些微服务中获取我们需要的数据,一个RPC服务其实就是一个数据源,API层的工作就是在聚合这些数据源,然后进行一些业务逻辑的处理,来提供接口供Client访问。具体的Mock代码可以在DataMock这个类中找到。
当然,有了数据源之后还需要进行一些业务逻辑的处理,本文使用一些Service来模拟这种处理,主要做的其实是将Author、Content以及Comment这三个Model联系起来,很好理解。
定义GraphQLOutputType
现在,你以及定义好了Model类了,并且已经有数据和业务逻辑处理程序了,下面就来定义一些GraphQLOutputType,这些GraphQLOutputType就是服务端可以提供的输出,你可以提供什么样的输出就怎么定义,下面首先展示的是AuthorModel这个GraphQLOutputType,然后展示了它的增强输出CompletableAuthor,可以作为参考:
/* basic outPutType */
private GraphQLOutputType author;
/* richness & completable outPutType */
private GraphQLOutputType completableAuthor;
/* The Author */
author = newObject().name("AuthorModel")
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorId").type(Scalars.GraphQLInt))
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorAge").type(Scalars.GraphQLInt))
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorLevel").type(Scalars.GraphQLInt))
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorAddr").type(Scalars.GraphQLString))
.field(GraphQLFieldDefinition.newFieldDefinition().name("friends").type(GraphQLList.list(Scalars.GraphQLInt)))
.build();
/* the completable author information */
completableAuthor = newObject().name("CompletableAuthor")
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorId").type(Scalars.GraphQLInt))
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorAge").type(Scalars.GraphQLInt))
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorLevel").type(Scalars.GraphQLInt))
.field(GraphQLFieldDefinition.newFieldDefinition().name("authorAddr").type(Scalars.GraphQLString))
.field(GraphQLFieldDefinition.newFieldDefinition().name("friends").type(GraphQLList.list(Scalars.GraphQLInt)))
.field(GraphQLFieldDefinition.newFieldDefinition().name("friendsCompletableInfo").type(GraphQLList.list(author)))
.field(GraphQLFieldDefinition.newFieldDefinition().name("contentModelList").type(GraphQLList.list(completableContent)))
.build();
完整的GraphQLOutputType定义可以参考项目(文章结尾)。上面有很多类似于“. type”的操作,GraphQL提供了很多类型,可以与各种语言中的类型系统进行对接,比如Scalars.GraphQLInt可以和Java中的Integer对接,而Scalars.GraphQLString和Java中的String对接,GraphQL除了支持这种Scalars类型外,还支持GraphList、Objects、以及Interfaces、Unions、Enums等,完整的类型系统可以参考文章GraphQL Type System,本文仅使用到了Scalars和GraphList。
定义Schema
定义好了一些GraphQLOutputType之后,就可以来定义GraphQL的Schema了,下面是本文使用的示例的Schema定义:
/* set up the schema */
schema = GraphQLSchema.newSchema()
.query(newObject()
.name("graphqlQuery")
.field(createAuthorField())
.field(createContentField())
.field(createCommentField())
.field(createCompletableContentField())
.field(createCompletableAuthorField()))
.build();
/**
* query single author
* @return the single author's information
*/
private GraphQLFieldDefinition createAuthorField() {
return GraphQLFieldDefinition.newFieldDefinition()
.name("author")
.argument(newArgument().name("authorId").type(Scalars.GraphQLInt).build())
.type(author)
.dataFetcher((DataFetchingEnvironment environment) -> {
//get the author id here
int authorId = environment.getArgument("authorId");
return this.authorService.getAuthorByAuthorId(authorId);
}).build();
}
/**
* completable author information
* @return the author
*/
private GraphQLFieldDefinition createCompletableAuthorField() {
return GraphQLFieldDefinition.newFieldDefinition()
.name("completableAuthor")
.argument(newArgument().name("authorId").type(Scalars.GraphQLInt).build())
.type(completableAuthor)
.dataFetcher((DataFetchingEnvironment environment) -> {
int authorId = environment.getArgument("authorId");
//get the completable info of author by authorId
//System.out.println("request for createCompletableAuthorField:" + authorId);
return authorService.getCompletableAuthorByAuthorId(authorId);
}).build();
}
上面只展示了author和completableAuthor两个GraphQLFieldDefinition的定义,服务端实际的聚合数据源的操作就需要写在这些GraphQLFieldDefinition里面,每个GraphQLFieldDefinition类似于一个服务端的API集合,并且它可以有一些入参,相当于restful的参数,你需要根据这些参数聚合DataSource来返回合适的数据。
提供查询接口
下面的代码展示了使用GraphQl来承接Client的查询请求的方法:
package io.hujian.graphql;
import graphql.GraphQL;
import java.util.Collections;
import java.util.Map;
/**
* Created by hujian06 on 2017/11/2.
*
* the facade of the graphQl
*/
public class GraphqlFacade {
private static final GraphqlProvider PROVIDER = new GraphqlProvider();
private static final GraphQL GRAPH_QL = GraphQL.newGraphQL(PROVIDER.getSchema()).build();
/**
* query by the Graphql
* @param ghql the query
* @return the result
*/
public static Map<String, Object> query(String ghql) {
if (ghql == null || ghql.isEmpty()) {
return Collections.emptyMap();
}
return GRAPH_QL.execute(ghql).getData();
}
}
提供接口
为了测试GraphQL,需要提供一个查询接口,下面的代码展示了如何使用Spring-boot来提供接口的方法:
package io.hujian.controller;
import com.alibaba.fastjson.JSON;
import io.hujian.graphql.GraphqlFacade;
import io.hujian.view.CheckView;
import io.hujian.view.MockerDataView;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by hujian06 on 2017/11/2.
*
* the graphql controller
*/
@Controller
@RequestMapping(value = "dsql/api/")
public class GraphqlController {
/**
* query the hsql by the graphql
* @param ghql the query string like:->
* "{
* author(authorId:2)
* {
* authorId,
* authorAge,
* authorAddr,
* friends
* }
* }"
* the response like:->
* "{
* "author": {
* "authorId": 2,
* "authorAge": 32,
* "authorAddr": "Ty-0021",
* "friends": [1]
* }
* }"
*
* @param request r
* @param response r
* @throws IOException e
*/
@RequestMapping(value = "query/{ghql}")
public void graphqlQuery(@PathVariable("ghql") String ghql, HttpServletRequest request, HttpServletResponse response)
throws IOException {
String result = JSON.toJSONString(GraphqlFacade.query(ghql));
System.out.println("request query:" + ghql + " \nresult:" + result);
//query the result.
response.getOutputStream().write(result.getBytes());
}
}
现在就可以来测试GraphQL是否可以正常工作了,先来一个简单的测试,比如,我们想要查询id为1的Author的信息,但是只想要知道AuthorAge以及AuthorLevel两个信息,查询的具体语句如下:
{
author(authorId:1) {
authorAge,
authorLevel
}
}
相应的查询结果如下:
{
"author": {
"authorAge": 24,
"authorLevel": 10
}
}
现在需求变了,Client不仅想要获取作者的年龄和级别,还想要知道作者的地址,那么服务端不需要改变任何内容,Client只需要改变Query就可以,新的Query为:
{
author(authorId:1) {
authorAge,
authorLevel,
authorAddr
}
}
这次查询的返回内容如下:
{
"author": {
"authorAge": 24,
"authorLevel": 10,
"authorAddr": "Fib-301"
}
}
为了说明GraphQL的强大,下面提供一个较为丰富复杂的查询以及其输出内容,首先展示了请求的响应内容:
{
"completableAuthor": {
"authorId": 1,
"authorLevel": 10,
"authorAge": 24,
"authorAddr": "Fib-301",
"friends": [
2,
3
],
"contentModelList": [
{
"contentId": 1,
"authorId": 1,
"text": "This is a test content!",
"commentModelList": [
{
"commentId": 2,
"authorId": 1,
"content": "i thing so."
}
]
}
],
"friendsCompletableInfo": [
{
"authorId": 2,
"authorAge": 32,
"authorLevel": 4,
"friends": [
1
]
},
{
"authorId": 3,
"authorAge": 14,
"authorLevel": 2,
"friends": [
2
]
}
]
}
}
对应的请求为:
{
completableAuthor(authorId:1) {
authorId,
authorLevel,
authorAge,
authorAddr,
friends,
contentModelList {
contentId,
authorId,
text,
commentModelList {
commentId,
authorId,
content
}
},
friendsCompletableInfo {
authorId,
authorAge,
authorLevel,
friends
}
}
}
结语
GraphQL不仅支持Query,还支持写操作,但是考虑到服务端API大部分的内容时聚合数据源而不是写数据,所以本文没有涉及相应的内容,但是后续的GraphQL系列中将会涉及GraphQL的所有支持的操作,并且分析这些操作的具体实现细节,最后,分享出本文涉及的项目的工程地址,如果不出意外,可以成功执行,注意设置application.properties,比如日志输出级别,服务器启动端口等,本文的项目的启动端口为8600,所以,如果你想要进行试验的话,需要在启动了项目之后再浏览器输入下面的地址:
http://127.0.0.1:8080/dsql/api/query/{your_query}
项目地址:GraphQL-Starter