一、背景
首先附上 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的方法
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