GraphQL初探:一種強大的DSQL

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容