相信各位開發者對於 GraphQL 帶來的好處已經非常清楚,如果對 GraphQL 很陌生的朋友們,可以直接參考之前作者寫的一篇『Go 語言實戰 GraphQL』,內容會講到用 Go 語言實戰 GraphQL 架構,教開發者如何撰寫 GraphQL 測試及一些開發小技巧,不過內容都是以 graphql-go 框架為主。而本篇主題會講為什麼我從 graphql-go 框架轉換到 gqlgen。
前言
我自己用 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
"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 使用:
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。底下可以拿使用者註冊來當做例子:
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 都可以很容易做到。
resolver:
layout: follow-schema
dir: graph
之後 gqlgen 會將目錄結構產生如下
user.graphql
user.resolver.go
cart.graphql
cart.resolver.go
開發者只要將相對應的 resolver method 實現出來就可以了。
強型別
如果有在寫 graphql-go 就可以知道該如何取得使用者 input 參數,在 graphql-go 使用的是 map[string]interface{}
型態,要正確拿到參數值,就必須要轉換型態
username := strings.ToLower(p.Args["username"].(string))
password := p.Args["password"].(string)
多了一層轉換相當複雜,而 gqlgen 則是直接幫忙轉成 struct 強型別
CreateUser(ctx context.Context, input model.CreateUserInput)
其中 model.CreateUserInput
就是完整的 struct,而並非是 map[string]interface{}
,在傳遞參數時,就不用多寫太多 interface 轉換,完整的註冊流程可以參考底下:
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 範例:
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 定義完整後,如下:
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 層級。
models:
User:
model: pkg/model.User
之後需要新增任何欄位,只要在 pkg/model.User
提供相對應的欄位或 method,重跑一次 gqlgen 就完成了。省下超多開發時間。
心得
其實 graphql-go 雷的地方不只有這些,還有很多地方沒有列出,但是上面的 gqlgen 優勢,已經足以讓我轉換到新的架構上。而在專案新的架構上,也同時具備 RESTFul API + GraphQL 設計,如果有時間再跟大家分享這部分。