相信各位開發者對於 GraphQL 帶來的好處已經非常清楚,如果對 GraphQL 很陌生的朋友們,可以直接參考之前作者寫的一篇『Go 語言實戰 GraphQL 』,內容會講到用 Go 語言 實戰 GraphQL 架構,教開發者如何撰寫 GraphQL 測試及一些開發小技巧,不過內容都是以 graphql-go 框架為主。而本篇主題會講為什麼我從 graphql-go 框架轉換到 gqlgen 。
教學影片 VIDEO
如果對於課程內容有興趣,可以參考底下課程。
如果需要搭配購買請直接透過 FB 聯絡我 ,直接匯款(價格再減 100 )
前言 我自己用 graphql-go 寫了一些專案,但是碰到的問題其實也不少,很多問題都可以在 graphql-go 專案的 Issue 列表 內都可以找到,雖然此專案的 Star 數量是最高,討論度也是最高,如果剛入門 GraphQL,需要練習,用這套見沒啥問題,比較資深的開發者,就不建議選這套了,先來看看功能比較圖
其中有幾項痛點是讓我主要轉換的原因:
效能考量 功能差異 schema first 強型別 自動產生程式碼 底下一一介紹上述特性
效能考量 我自己建立效能 Benchamrk 來比較市面上幾套 GraphQL 套件 golang-graphql-benchmark
graphql-go/graphql version: v0.7.9
playlyfe/go-graphql version: v0.0.0-20191219091308-23c3f22218ef graph-gophers/graphql-go version: v0.0.0-20200207002730-8334863f2c8b samsarahq/thunder version: v0.5.0 99designs/gqlgen version: v0.11.3
Requests/sec graphql-go 19004.92 graph-gophers 44308.44 thunder 40994.33 gqlgen 49925.73
由上面可以看到光是一個 Hello World 範例,最後的結果由 gqlgen 勝出,現在討論度比較高的也只有 gqlgen 跟 grapgql-go,效能上面差異頗大。這算是我轉過去的最主要原因之一。
功能差異 幾個重點差異,底下看看比較圖:
Type Safety Type Binding Upload FIle 等蠻多細部差異,graphql-go 目前不支持檔案上傳,所以還是需要透過 RESTFul API 方式上傳,但是已經有人提過 Issue 且發了 PR , 作者看起來沒有想處理這題。就拿上傳檔案當做例子,在 gqlgen 寫檔案上傳相當容易,先寫 schema
1
2
3
4
5
6
7
8
9
10
"The `Upload` scalar type represents a multipart file upload."
scalar Upload
"The `File` type, represents the response of uploading a file."
type File {
name: String!
contentType: String!
size: Int!
url: String!
}
就可以直接在 resolver 使用:
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
type File struct {
Name string
Size int
Content []byte
ContentType string
}
func (r *mutationResolver) getFile (file graphql.Upload) (*File, error ) {
content, err := ioutil.ReadAll (file.File)
if err != nil {
return nil , errors.EBadRequest (errorUploadFile, err)
}
contentType := ""
kind, _ := filetype.Match (content)
if kind != filetype.Unknown {
contentType = kind.MIME.Value
}
if contentType == "" {
contentType = http.DetectContentType (content)
}
return &File{
Name: file.Filename,
Size: int (file.Size),
Content: content,
ContentType: contentType,
}, nil
}
Schema first 後端設計 API 時需要針對使用者情境及 Database 架構來設計 GraphQL Schema,詳細可以參考 Schema Definition Language 。底下可以拿使用者註冊來當做例子:
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
enum EnumGender {
MAN
WOMAN
}
# Input Types
input createUserInput {
email: String!
password: String!
doctorCode: String
}
type createUserPayload {
user: User
actCode: String
digitalCode: String
}
# Types
type User {
id: ID
email: String!
nickname: String
isActive: Boolean
isFirstLogin: Boolean
avatarURL: String
gender: EnumGender
}
type Mutation {
createUser(input: createUserInput!): createUserPayload
}
除了可以先寫 Schema 之外,還可以根據不同情境的做分類,將一個完整的 Schema 拆成不同模組,這個在 gqlgen 都可以很容易做到。
1
2
3
resolver :
layout : follow-schema
dir : graph
之後 gqlgen 會將目錄結構產生如下
1
2
3
4
user.graphql
user.resolver.go
cart.graphql
cart.resolver.go
開發者只要將相對應的 resolver method 實現出來就可以了。
強型別 如果有在寫 graphql-go 就可以知道該如何取得使用者 input 參數,在 graphql-go 使用的是 map[string]interface{}
型態,要正確拿到參數值,就必須要轉換型態
1
2
username := strings.ToLower (p.Args["username" ].(string ))
password := p.Args["password" ].(string )
多了一層轉換相當複雜,而 gqlgen 則是直接幫忙轉成 struct 強型別
1
CreateUser (ctx context.Context, input model.CreateUserInput)
其中 model.CreateUserInput
就是完整的 struct,而並非是 map[string]interface{}
,在傳遞參數時,就不用多寫太多 interface 轉換,完整的註冊流程可以參考底下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (r *mutationResolver) CreateUser (ctx context.Context, input model.CreateUserInput) (*model.CreateUserPayload, error ) {
resp, err := api.CreateUser (r.Config, api.ReqCreateUser{
Email: input.Email,
Password: input.Password,
})
if err != nil {
return nil , err
}
return &model.CreateUserPayload{
User: resp.User,
DigitalCode: convert.String (resp.DigitalCode),
ActCode: convert.String (resp.ActCode),
}, nil
}
自動產生代碼 要維護欄位非常多的 Schema 相當不容易,在 graphql-go 每次改動欄位,都需要開發者自行修改,底下是 user type 範例:
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
var userType = graphql.NewObject (graphql.ObjectConfig{
Name: "UserType" ,
Description: "User Type" ,
Fields: graphql.Fields{
"id" : &graphql.Field{
Type: graphql.ID,
},
"email" : &graphql.Field{
Type: graphql.String,
},
"username" : &graphql.Field{
Type: graphql.String,
},
"name" : &graphql.Field{
Type: graphql.String,
},
"isAdmin" : &graphql.Field{
Type: graphql.Boolean,
Resolve: func (p graphql.ResolveParams) (interface {}, error ) {
source := p.Source
o, ok := source.(*model.User)
if !ok {
return false , nil
}
return o.CheckAdmin (), nil
},
},
"isNewcomer" : &graphql.Field{
Type: graphql.Boolean,
},
"createdAt" : &graphql.Field{
Type: graphql.DateTime,
},
"updatedAt" : &graphql.Field{
Type: graphql.DateTime,
},
},
})
上面這段程式碼是要靠開發者自行維護,只要有任何異動,都需要手動自行修改,但是在 gqlgen 就不需要了,你只要把 schema 定義完整後,如下:
1
2
3
4
5
6
7
8
9
type User {
id: ID
email: String!
username: String
isAdmin: Boolean
isNewcomer: Boolean
createdAt: Time
updatedAt: Time
}
在 console 端下 go run github.com/99designs/gqlgen
,就會自動將代碼生成完畢。你也可以將 User 綁定在開發者自己定義的 Model 層級。
1
2
3
models :
User :
model : pkg/model.User
之後需要新增任何欄位,只要在 pkg/model.User
提供相對應的欄位或 method,重跑一次 gqlgen 就完成了。省下超多開發時間。
心得 其實 graphql-go 雷的地方不只有這些,還有很多地方沒有列出,但是上面的 gqlgen 優勢,已經足以讓我轉換到新的架構上。而在專案新的架構上,也同時具備 RESTFul API + GraphQL 設計,如果有時間再跟大家分享這部分。
See also