Graphql 調研

OkidoGreen發表於2020-04-05

一、背景

首先附上 Graphsql中文官網地址 及 對應的 JavaDoc地址
簡單介紹一下背景,公司內部有很多B端運營類專案,由於後期的不斷迭代,後端的Rest服務介面(Ajax)變得越來越多,恰好適逢前端重構,故領導想試試看能否使用對前端來說適應及使用性更便捷的Graphql來替換Rest。後端的重構就落到我這邊了。

二、介紹

GraphQL可以在原本的前端-後端的呼叫鏈中新增一箇中間層BFF,用來對後端微服務的資料進行整合。
BFF:Backend for Frontends(以下簡稱BFF) 顧名思義,是為前端而存在的後端(服務)中間層。即傳統的前後端分離應用中,前端應用直接呼叫後端服務,後端服務再根據相關的業務邏輯進行資料的增刪查改等。那麼引用了 BFF 之後,前端應用將直接和 BFF 通訊,BFF 再和後端進行 API 通訊,所以本質上來說,BFF 更像是一種“中間層”服務。

關於IDL(介面描述語言)以及Graphql的語法文件,大家還是看官網,這邊簡單的介紹一下

語言模式

圖語言,用“節點”“關係”來描述一組資料結構,與常規資料庫join的理念不同

操作分類

query:查詢
mutation:增刪改

構成

Schema/GraphQLSchema:

定義所有可供查詢的欄位(field),它們最終組合成一套完整的GraphQL API
Schema相當於一個資料庫,它有很多GraphQLFieldDefinition組成,Field相當於資料庫表/檢視,每個表/檢視又由名稱、查詢引數、資料結構、資料組成。同時它定義了一個請求可以返回的資料格式與描述

Types

GraphQL 型別系統支援以下型別

  • Scalar/基礎型別
  • Object/GraphQLObjectType
  • Interface/GraphQLInterfaceType
  • Union/GraphQLUnionType
  • InputObject/GraphQLInputObjectType //專門用於定義輸入引數型別
  • Enum/GraphQLEnumType

Scalar

graphql-java 支援以下基本資料型別( Scalars)

  • GraphQLBoolean
  • GraphQLInt
  • GraphQLFloat
  • GraphQLID
  • GraphQLLong
  • GraphQLShort
  • GraphQLByte
  • GraphQLFloat
  • GraphQLBigDecimal
  • GraphQLBigInteger

Object/GraphQLObjectType

物件欄位型別,定義了一個資料模型,類似資料表中的每一列的欄位定義

Field/GraphQLFieldDefinition

欄位型別定義,可用於物件型別中的欄位設定,及請求物件上的特定欄位(可根據該欄位定義返回型別,匹配引數等)

Arguments/GraphQLArgument

每一個GraphQLFieldDefinition可提供的查詢條件,可設定引數值,用於查詢匹配

DataFetcher

資料返回獲取器,實現Field欄位上的資料返回介面,可在environment中獲取查詢引數

DataFetchingEnvironment

資料獲取上下文,可以拿到對應的查詢引數

GraphQL.execute

最終查詢執行器
簡易demo可參考範例graphql學習(四)GraphQL和SpringMVC 的整合

三、個人理解

官網上看了一下Hello World的Demo和他人基於Graphql寫的一些blog,簡單總結了一些優缺點和使用上的疑問:

優點

  • 清晰的資料模型,欄位強型別
  • 前端-按需獲取,減少網路請求
  • API迭代順暢,無須版本化
  • 協議而非儲存,對服務端資料進行組裝過濾

Rest比較

  • 資料獲取:Rest缺乏擴充套件性,GraphQL獲取時,payload可以擴充套件,按需獲取
  • API呼叫:Rest有多個endpoint,GraphQL在大多數情況下只有1個endpoint,只是body內容不同
  • 複雜請求:Rest需要多次,GraphQL一次呼叫,減少網路開銷
  • 返回處理:Rest有多種httpCode及Status,GraphQL只有200響應,錯誤內容需要在結果集中特殊獲取
  • 版本號:Rest使用V1、V2,GraphQL可根據Schema自行擴充套件

可以看出,優點中大部分都是對前端開發有利,那後端改造的優勢是什麼呢?簡單看了幾個例子後發現,後端如果使用Graphql重構,就相當於服務層做了一層類似資料庫的DDL+DML的抽象和封裝。
Graphql使用了自身API構造了和資料庫雷同的DDL,包含表結構(整體schema)、可查詢欄位(Field)、行列資訊(Object),可變入參(Variable),形式極似Sql的select語句。但如此一來,在使用上就有以下的疑問了

疑問

  • 官網標榜的精確資料返回和一個請求如何實現?表面上看似返回的內容會隨著請求實體變化,但實際上,由於無法確定前端查詢的內容及引數,對於後端來說,還是需要獲取全量的資料結果,只是在構建Graphql自己的“資料庫”時定義不同的查詢schema罷了。原先的“select a,b”,在Graphql上不就變成了“select * ” ? 這樣對資料庫及其他內部服務的壓力必然會增大,記憶體&快取解決?
  • 其次,對於一個完整的系統,Graphql要求的資料模型必然需要對業務精確的理解,需要提前定義完整的資料結構,每一個返回的實體都必須單獨定義一個Schema及配套的查詢方法。這樣才能準確的定義服務端的Schema,供前端使用。
  • 對於資料校驗、使用者許可權及資料安全性來說解決方案也比較模糊,由於開放了“select *”的功能,是否會造成全量的欄位查詢導致資料暴露
  • 文件比較匱乏,使用案例也較少

四、實踐

兩種定義schema的方法

  • Java
  • IDL

Example:實際使用的時候有一點需要注意,許多官網例子中一些方法都是import static方式引入的Class,有些方法在idea中很難搜到。大GraphQL中的物件構造方式大多都是建造者模式,也是Effect java中推薦的多引數的物件構造方式,大家可以自己看一下

Java

1
2
3
4
5
6
import static graphql.Scalars.GraphQLString;
import static graphql.schema.AsyncDataFetcher.async;
import static graphql.schema.GraphQLArgument.newArgument;
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
import static graphql.schema.GraphQLObjectType.newObject;
import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//假設現在有 user pojo ,dog pojo,user中有list[dog],類似一對多的關係
//自定義使用者型別 及 欄位
public static GraphQLObjectType getUserType(){
    GraphQLObjectType userType = newObject()
            .name("user")
            .field(newFieldDefinition().name("id").type(Scalars.GraphQLInt).build())
            .field(newFieldDefinition().name("age").type(Scalars.GraphQLInt).build())
            .field(newFieldDefinition().name("userName").type(GraphQLString).build())
            .field(newFieldDefinition().name("dogs").type(new GraphQLList(getDogType())).build())
            .build();
    return userType;
}
//dog type
public static GraphQLObjectType getDogType(){
    GraphQLObjectType dogType = newObject()
            .name("dog")
            .field(newFieldDefinition().name("id").type(Scalars.GraphQLInt).build())
            .field(newFieldDefinition().name("dogName").type(GraphQLString).build())
            .build();
    return dogType;
}
//定義一個查詢Field 欄位 及可查詢引數與返回值型別 & 一個資料獲取器 dataFetch
public static GraphQLFieldDefinition userQuery(){
    DataFetcher userDataFetcher = async(environment -> {
        // 獲取查詢引數
        Integer id = environment.getArgument("id");
        Integer start = environment.getArgument("start");
        Integer limit = environment.getArgument("limit");
        System.out.println("GraphQLFieldDefinition query ,id = " + id
                + " ,start = "+start+ " ,limit="+limit);
        // 執行查詢, 這裡隨便用一些測試資料來說明問題
        //getObj 自己組裝List user 資料
        List<User> result = getObj();
        System.out.println(result);
        return result;
    });
    return GraphQLFieldDefinition.newFieldDefinition()
            .name("users")
            .argument(newArgument().name("id").type(new GraphQLNonNull(Scalars.GraphQLInt)).build())
            .argument(newArgument().name("start").type(Scalars.GraphQLInt).build())
            .argument(newArgument().name("limit").type(Scalars.GraphQLInt).build())
            .type(new GraphQLList(getUserType()))
            .dataFetcher(userDataFetcher)
            .build();
}
//schema 定義 ,繫結 userQuery這個欄位的請求查詢
GraphQLSchema schema = GraphQLSchema.newSchema().query(newObject()
            .name("GraphQuery")
            .field(userQuery())
            .build()).build();
//執行處理器,採用Future的方式非同步獲取查詢結果,可使用JAVA8的lambda函式x
GraphQL graphQL = GraphQL.newGraphQL(schema).queryExecutionStrategy(new AsyncExecutionStrategy())
            .mutationExecutionStrategy(new AsyncSerialExecutionStrategy()).build();
    ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query1).build();
    CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);
    //ExecutionResult executionResult = graphQL.execute(executionInput);
    //promise.join();
    Future<ExecutionResult> f = promise.whenComplete((v, e) -> {
        System.out.println("Future: result " + v.getData());
        System.out.println("Error: result " + v.getErrors());
        System.out.println("Ex: result " + v.getExtensions());
        e.printStackTrace();
    });
    promise.thenAccept(executionResult -> {
        // here you might send back the results as JSON over HTTP
        System.out.println("Future: result " + executionResult.getData());
    });
    Thread.sleep(10000);

輸出結果:

1
2
3
4
5
6
GraphQLFieldDefinition query ,id = 2 ,start = 5 ,limit=10
[graphql.User@400989ba]
Future: result {users=[{id=1, userName=2854bfbf-7037-4854-a470-53360b49f1fb, dogs=[{id=100, dogName=Dog52bef01d-99d8-4712-afaa-c046d61976ab}]}]}
Error: result []
Ex: result null
Future: result {users=[{id=1, userName=2854bfbf-7037-4854-a470-53360b49f1fb, dogs=[{id=100, dogName=Dog52bef01d-99d8-4712-afaa-c046d61976ab}]}]}

 

IDL

users.graphqls(編譯後在classpath下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
schema {
    query: GraphQuery2
}
type GraphQuery2 {
    users(id: Int,start: Int,limit: Int): [User]
}
type User {
    id: Int
    age: Int
    userName: String!
    dogs: [Dog]
}
type Dog {
    id: Int
    dogName: String!
}

 

java程式碼(之前對於user,dog的物件型別定義都可以去除,請求Field及schema同樣也可以不要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//載入classpath下的IDL檔案
private static File loadSchema(final String s) {
        System.out.println(GraphqlTest2.class.getClassLoader().getResource("graphql/users.graphqls"));
        return new File(GraphqlTest2.class.getClassLoader().getResource(s).getFile());
    }
private static DataFetcher userDataFetcher = async(environment -> {
    // 獲取查詢引數
    Integer id = environment.getArgument("id");
    Integer start = environment.getArgument("start");
    Integer limit = environment.getArgument("limit");
    System.out.println("GraphQLFieldDefinition query ,id = " + id
            + " ,start = "+start+ " ,limit="+limit);
    // 執行查詢, 這裡隨便用一些測試資料來說明問題
    List<User> result = getObj();
    System.out.println(result);
    return result;
});
//執行環境構建,同時註冊一個schema查詢及對應的field獲取器
private static RuntimeWiring buildRuntimeWiring() {
    //return RuntimeWiring.newRuntimeWiring().wiringFactory(new EchoingWiringFactory()).build();
    return RuntimeWiring.newRuntimeWiring()
                    // this uses builder function lambda syntax
            .type("GraphQuery2", typeWiring -> typeWiring
                            .dataFetcher("users", userDataFetcher)
            ).build();
}
public static void main(String[] args) throws InterruptedException {
    SchemaParser schemaParser = new SchemaParser();
    SchemaGenerator schemaGenerator = new SchemaGenerator();
    File schemaFile = loadSchema("graphql/users.graphqls");
    TypeDefinitionRegistry typeRegistry = schemaParser.parse(schemaFile);
    RuntimeWiring wiring = buildRuntimeWiring();
    //等同於java構建schema的mainExec方法
    GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, wiring);
    //$xxx 使用了查詢變數,在執行實際查詢時可以傳入實際的variable進行替換(Map),同時在fetch資料時,在environment中獲取,供持久層使用
    String query1 = "query GraphQuery2($testUserId:Int) {users(id:$testUserId,start:5,limit:10) " +
            " {id,userName,dogs{id,dogName}}}";
    GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).queryExecutionStrategy(new AsyncExecutionStrategy())
            .mutationExecutionStrategy(new AsyncSerialExecutionStrategy()).build();
    Map<String,Object> variable = Maps.newHashMap();
    variable.put("testUserId",12345);
    //graphQL.execute(query, null, null, variables); deprecated
    ExecutionInput executionInput = ExecutionInput.newExecutionInput().variables(variable).query(query1).build();
    CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);
    //ExecutionResult executionResult = graphQL.execute(executionInput);
    //promise.join();
    Future<ExecutionResult> f = promise.whenComplete((v, e) -> {
        System.out.println("Future: result " + v.getData());
        System.out.println("Error: result " + v.getErrors());
        System.out.println("Ex: result " + v.getExtensions());
        e.printStackTrace();
    });
    promise.thenAccept(executionResult -> {
        // here you might send back the results as JSON over HTTP
        System.out.println("Future: result " + executionResult.getData());
    });
    Thread.sleep(10000);
}

 

輸出結果與JAVA方式一致
結論:可以看到,IDL中可以將schema的query名稱、自定義物件型別、schema內的請求查詢Field統一包括,對比Java定義的方式節省很多程式碼及工作量,而且定義好的檔案也可供前端參考。

Todo

  • 異常處理&引數校驗
  • SpringBoot整合
  • 資料變更mutation嘗試

結束

總結一下整個流程,基本上就是
定義GrapQL資料型別 -> 定義暴露給客戶端的query api和mutaion api -> 建立GraphQL Schema

參考

GraphQL
官方文件翻譯
微服務下使用GraphQL構建BFF
GraphQL和SpringMVC 的整合
GraphQL這個坑可以入了
GraphQL+Java實戰
Java Code Examples for graphql.schema.idl.SchemaGenerator
SpringMVC+graphql Demo
Demo2
Demo3
Interface使用Demo

相關文章