作者: 一字馬胡
轉載標志 【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