GraphQL初探:一種強(qiáng)大的DSQL

作者: 一字馬胡
轉(zhuǎn)載標(biāo)志 【2017-11-03】

更新日志

日期 更新內(nèi)容 備注
2017-11-03 新建文章 初版

初識(shí)GraphQL

GraphQL是一種強(qiáng)大的DSQL,是由Facebook開源的一種用于提供數(shù)據(jù)查詢服務(wù)的抽象框架,在服務(wù)端API開發(fā)中,很多時(shí)候定義一個(gè)接口返回的數(shù)據(jù)相對(duì)固定的,如果想要獲取更多的信息,或者僅需要某個(gè)接口的某個(gè)信息的時(shí)候,基于restful API的接口就顯得不那么靈活了,對(duì)于這些需求,服務(wù)端要么再定義一個(gè)新的接口,返回合適的數(shù)據(jù),要么客戶端就得通過一個(gè)龐大的接口來獲取一小部分信息,GraphQL的出現(xiàn)就是為了解決這些問題的,GraphQL并不是一門具體的語言實(shí)現(xiàn)的某種框架,它是一系列協(xié)議文檔組成的項(xiàng)目,GraphQL是和語言無關(guān)的,而且到現(xiàn)在為止已經(jīng)有很多語言的實(shí)現(xiàn)版本,可以在awesome-graphql看到哪些語言實(shí)現(xiàn)了GraphQL,如果想要了解具體的GraphQL定義,可以參考graphql。本文以及本GraphQL系列將只關(guān)心Java版本的GraphQL實(shí)現(xiàn),具體的Java版本的GraphQL可以參考graphql-java。下面是官方對(duì)GraphQL的描述,很簡潔,但是很直觀:

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.

下面的圖片展示了GraphQL的工作模型:

從這張圖片可以看出,GraphQL的位置處于Client和DataSource之間,可以把這一層理解為服務(wù)端的API層,所謂API層,就是聚合多個(gè)數(shù)據(jù)源,進(jìn)行一些業(yè)務(wù)邏輯的處理,然后提供一些接口給Client調(diào)用。而GraphQL就工作在這一層,它相當(dāng)于是對(duì)DataSource的一層抽象,它可以承接Client的請(qǐng)求,然后根據(jù)GraphQL的執(zhí)行引擎來從DataSource獲取數(shù)據(jù),然后進(jìn)行處理之后返回json結(jié)果給Client,這和Restful的模式?jīng)]有什么差別,但是GraphQL的強(qiáng)大之處在于GraphQL類似于MySql,Client發(fā)送的請(qǐng)求類似于Sql語句,這些Sql語句經(jīng)過GraphQL解析執(zhí)行之后返回具體的數(shù)據(jù),所以GraphQL具有很好的動(dòng)態(tài)性,Client可以根據(jù)不同的需求來使用不同的Sql語句來請(qǐng)求服務(wù)端,而GraphQL會(huì)解析這些Sql,并且精準(zhǔn)的返回結(jié)果。這就完美的解決了文章開頭提到的難題。使用GraphQL來做服務(wù)端API層的開發(fā)無疑會(huì)減輕服務(wù)端開發(fā)工程師的很多壓力,而且對(duì)于Client來說也是很友好的,因?yàn)镃lient不需要想請(qǐng)求Restful接口一樣只能獲取相對(duì)固定的數(shù)據(jù),Client可以根據(jù)自己的需求使用不同的查詢語句來請(qǐng)求GraphQL,使用GraphQL會(huì)減少很多冗余的數(shù)據(jù)傳輸,并且可以減少很多服務(wù)端API層的接口開發(fā)工作,API層只需要開發(fā)GraphQL服務(wù)端,然后告訴Client這些數(shù)據(jù)的組織結(jié)構(gòu),然后Client就可以組裝出合適的查詢語句來請(qǐng)求數(shù)據(jù)。使用GraphQL進(jìn)一步將前后端分離(Restful使得前后端分離),后端開發(fā)和前端開發(fā)可以各自進(jìn)行,使用GraphQL很多時(shí)候服務(wù)端是在豐富可以提供的數(shù)據(jù),或者優(yōu)化聚合DataSource來提高響應(yīng)速度。使用GraphQL還有很多優(yōu)點(diǎn),可以研究GraphQL并且使用GraphQL來開發(fā)服務(wù)端API來體驗(yàn)。本文剩下的內(nèi)容將基于GraphQL-Java和Spring-boot來實(shí)現(xiàn)一個(gè)簡單的應(yīng)用,以此來說明使用GraphQL的方法以及使用GraphQL的優(yōu)勢(shì)。

需要補(bǔ)充的一點(diǎn)是,上面提到了GraphQL查詢語句(上文使用了Sql代替,但不是Sql),這是一種類似于json的結(jié)構(gòu)化數(shù)據(jù),可以很輕易的理解它的本意,這也是GraphQL的一個(gè)優(yōu)點(diǎn),它的查詢語句對(duì)工程師是很友好的。下文會(huì)分析到。

GraphQL 實(shí)戰(zhàn)

本GraphQL系列的文章基于Java語言以及GraphQL-Java來分析,這一點(diǎn)注意一下。本文的GraphQL示例使用Spring-boot來開發(fā),使用的IDE為idea 17,強(qiáng)烈建議Javaer使用IDEA來開發(fā),可以明顯提高開發(fā)效率。

為了可以快速上手,下面展示了本文使用的示例的代碼結(jié)構(gòu):

可以根據(jù)各個(gè)包名來理解這個(gè)包管理的類,比如service管理的是一系列service,而view包下是一些需要返回給Client的渲染View。關(guān)于如何新建一個(gè)Spring-boot項(xiàng)目的過程不再本文的敘述范圍之內(nèi)(唯一說明的一點(diǎn)是,需要Web模塊支持),下面根據(jù)一些關(guān)鍵步驟來引導(dǎo)如何實(shí)現(xiàn)一個(gè)GraphQL demo。

創(chuàng)建Model類

這一步很簡單,將你需要?jiǎng)?chuàng)建的Model類放到model包下,比如本文的示例想要實(shí)現(xiàn)的一個(gè)場(chǎng)景是,有一些作者,每個(gè)作者可能寫了多篇文章,每篇文章都只有一個(gè)作者,而每篇文章下面可能沒有評(píng)論,或者有評(píng)論,評(píng)論的數(shù)量不限,下面是幾個(gè)關(guān)鍵的類信息:

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
}

為了實(shí)驗(yàn)GraphQL的復(fù)雜查詢,下面是兩個(gè)增強(qiáng)類,分別是對(duì)AuthorModel類和ContentModel類的增強(qiáng),可以看到增強(qiáng)之后的類更符合我們的想法:


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數(shù)據(jù)

為了測(cè)試GraphQL,你需要有一些數(shù)據(jù),本文為了快速測(cè)試GraphQL,所以Mock的數(shù)據(jù)比較簡單,沒有和數(shù)據(jù)庫交互,其實(shí)在真實(shí)的服務(wù)端API層開發(fā)中,很多時(shí)候是不需要和數(shù)據(jù)庫交互的,更多的是使用RPC來從一些微服務(wù)中獲取我們需要的數(shù)據(jù),一個(gè)RPC服務(wù)其實(shí)就是一個(gè)數(shù)據(jù)源,API層的工作就是在聚合這些數(shù)據(jù)源,然后進(jìn)行一些業(yè)務(wù)邏輯的處理,來提供接口供Client訪問。具體的Mock代碼可以在DataMock這個(gè)類中找到。

當(dāng)然,有了數(shù)據(jù)源之后還需要進(jìn)行一些業(yè)務(wù)邏輯的處理,本文使用一些Service來模擬這種處理,主要做的其實(shí)是將Author、Content以及Comment這三個(gè)Model聯(lián)系起來,很好理解。

定義GraphQLOutputType

現(xiàn)在,你以及定義好了Model類了,并且已經(jīng)有數(shù)據(jù)和業(yè)務(wù)邏輯處理程序了,下面就來定義一些GraphQLOutputType,這些GraphQLOutputType就是服務(wù)端可以提供的輸出,你可以提供什么樣的輸出就怎么定義,下面首先展示的是AuthorModel這個(gè)GraphQLOutputType,然后展示了它的增強(qiáng)輸出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定義可以參考項(xiàng)目(文章結(jié)尾)。上面有很多類似于“. type”的操作,GraphQL提供了很多類型,可以與各種語言中的類型系統(tǒng)進(jìn)行對(duì)接,比如Scalars.GraphQLInt可以和Java中的Integer對(duì)接,而Scalars.GraphQLString和Java中的String對(duì)接,GraphQL除了支持這種Scalars類型外,還支持GraphList、Objects、以及Interfaces、Unions、Enums等,完整的類型系統(tǒng)可以參考文章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兩個(gè)GraphQLFieldDefinition的定義,服務(wù)端實(shí)際的聚合數(shù)據(jù)源的操作就需要寫在這些GraphQLFieldDefinition里面,每個(gè)GraphQLFieldDefinition類似于一個(gè)服務(wù)端的API集合,并且它可以有一些入?yún)ⅲ喈?dāng)于restful的參數(shù),你需要根據(jù)這些參數(shù)聚合DataSource來返回合適的數(shù)據(jù)。

提供查詢接口

下面的代碼展示了使用GraphQl來承接Client的查詢請(qǐng)求的方法:


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();
    }

}

提供接口

為了測(cè)試GraphQL,需要提供一個(gè)查詢接口,下面的代碼展示了如何使用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());
    }

}

現(xiàn)在就可以來測(cè)試GraphQL是否可以正常工作了,先來一個(gè)簡單的測(cè)試,比如,我們想要查詢id為1的Author的信息,但是只想要知道AuthorAge以及AuthorLevel兩個(gè)信息,查詢的具體語句如下:


{
  author(authorId:1) {
     authorAge,
     authorLevel
   }
}

相應(yīng)的查詢結(jié)果如下:


{
    "author": {
        "authorAge": 24,
        "authorLevel": 10
    }
}

現(xiàn)在需求變了,Client不僅想要獲取作者的年齡和級(jí)別,還想要知道作者的地址,那么服務(wù)端不需要改變?nèi)魏蝺?nèi)容,Client只需要改變Query就可以,新的Query為:


{
  author(authorId:1) {
     authorAge,
     authorLevel,
     authorAddr
   }
}

這次查詢的返回內(nèi)容如下:


{
    "author": {
        "authorAge": 24,
        "authorLevel": 10,
        "authorAddr": "Fib-301"
    }
}

為了說明GraphQL的強(qiáng)大,下面提供一個(gè)較為豐富復(fù)雜的查詢以及其輸出內(nèi)容,首先展示了請(qǐng)求的響應(yīng)內(nèi)容:

{
    "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
                ]
            }
        ]
    }
}

對(duì)應(yīng)的請(qǐng)求為:


{
     completableAuthor(authorId:1) {
     authorId,
     authorLevel,
     authorAge,
     authorAddr,
     friends,
     contentModelList {
     contentId,
     authorId,
     text,
     commentModelList {
       commentId,
       authorId,
       content
     }
   },
     friendsCompletableInfo {
       authorId,
       authorAge,
       authorLevel,
       friends
     }
   }
}

結(jié)語

GraphQL不僅支持Query,還支持寫操作,但是考慮到服務(wù)端API大部分的內(nèi)容時(shí)聚合數(shù)據(jù)源而不是寫數(shù)據(jù),所以本文沒有涉及相應(yīng)的內(nèi)容,但是后續(xù)的GraphQL系列中將會(huì)涉及GraphQL的所有支持的操作,并且分析這些操作的具體實(shí)現(xiàn)細(xì)節(jié),最后,分享出本文涉及的項(xiàng)目的工程地址,如果不出意外,可以成功執(zhí)行,注意設(shè)置application.properties,比如日志輸出級(jí)別,服務(wù)器啟動(dòng)端口等,本文的項(xiàng)目的啟動(dòng)端口為8600,所以,如果你想要進(jìn)行試驗(yàn)的話,需要在啟動(dòng)了項(xiàng)目之后再瀏覽器輸入下面的地址:

http://127.0.0.1:8080/dsql/api/query/{your_query}

項(xiàng)目地址:GraphQL-Starter

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容