Go語言的GraphQL實踐總結
GraphQL背景
REST API的使用方式是,server定義一系列的介面,client呼叫自己需要的介面,獲取目標資料進行整合。REST API開發中遇到的問題:
- 擴充套件性 ,隨著API的不斷髮展,REST API的介面會變得越來臃腫。
- 無法按需獲取 ,一個返回id, name, age, city, addr, email的介面,如果僅獲取部分資訊,如name, age,卻必須返回介面的全部資訊,然後從中提取自己需要的。壞處不僅會增加網路傳輸量,並且不便於client處理資料
- 一個請求無法獲取所需全部資源 ,例如client需要顯示一篇文章的內容,同時要顯示評論,作者資訊,那麼就需要呼叫文章、評論、使用者的介面。壞處造成服務的的維護困難,以及響應時間變長
- 原因: REST API通常由多個端點組成,每個端點代表一種資源。所以,當client需要多個資源是,它需要向REST API發起多個請求,才能獲取到所需要的資料。
- REST API不好處理的問題 , 比如確保client提供的引數是型別安全的,如何從程式碼生成API的文件等。
GraphQL解決的問題:
- 請求你的資料不多不少 :GraphQL查詢總是能準確獲得你想要的資料,不多不少,所以返回的結果是可預測的。
- 獲取多個資源只用一個請求 :GraphQL查詢不僅能夠獲得資源的屬性,還能沿著資源間進一步查詢,所以GraphQL可以通過一次請求就獲取你應用所需的所有資料。
- 描述所有的可能型別系統
- 使用你現有的資料和程式碼: GraphQL讓你的整個應用共享一套API,通過GraphQL API能夠更好的利用你的現有資料和程式碼。GraphQL 引擎已經有多種語言實現,GraphQL不限於某一特定資料庫,可以使用已經存在的資料、程式碼、甚至可以連線第三方的APIs。
- API 演進無需劃分版本: 給GraphQL API新增欄位和型別而無需影響現有查詢。老舊欄位可以廢棄,從工具中隱藏。
什麼是GraphQL
GraphQL官網給出定義:GraphQL既是一種用於API的查詢語言
- API不是用來呼叫的嗎?是的,者正是GraphQL的強大之處,引用官方文件的一句話
ask exactly what you want
- 本質上來說GraphQL是一種查詢語言
- 上述的定義其實很難理解,只有真的使用過GraphQL才能夠理解。
在GraphQL中,通過定義一張Schema和宣告一些Type來達到上述描述的功能,需要學習:
- 對於資料模型的抽象是通過Type來描述的 ,如何定義Type?
- 對於介面獲取資料的邏輯是通過schema來描述的 ,如何定義schema?
如何定義Type
對於資料模型的抽象是通過Type來描述的,每一個Type有若干Field組成,每個Field又分別指向某個Type。
GraphQL的Type簡單可以分為兩種,一種是scalar type(標量型別) ,另一種是object type(物件型別)。
scalar type
GraphQL中的內建的標量包含,String、Int、Float、Boolean、Enum,除此之外,GraphQL中可以通過scalar宣告一個新的標量 ,比如:
- prisma ——一個使用GraphQL來抽象資料庫操作的庫中,還有DataTime(日期格式)和主鍵(ID)。
- 在使用GraphQL實現檔案上傳介面時,需要宣告一個Upload標量來代表要上傳的檔案。
- 標量是GraphQL型別系統中最小的顆粒。
object type
僅有標量是不夠抽象一些複雜的資料模型,這時需要使用物件型別。通過物件型別來構建GraphQL中關於一個數據模型的形狀,同時還可以宣告各個模型之間的內在關聯(一對多,一對一或多對多)。
一對一模型
type Article {
id: ID
text: String
isPublished: Boolean
author: User
}
上述程式碼,聲明瞭一個Article型別,它有3個Field,分別是id(ID型別)、text(String型別)、isPublished(Boolean型別)以及author(新建的物件型別User),User型別的宣告如下:
type User {
id: ID
name: String
}
lType Modifier
型別修飾符,當前的型別修飾符有兩種,分別是List和Required ,語法分別為[Type]和[Type!],兩者可以組合:
- [Type]! :列表本身為必填項,但內部元素可以為空
- [Type!] :列表本身可以為空,但是其內部元素為必填
- [Type!]! :列表本身和內部元素均為必填
如何定義Schema
schema用來描述對於介面獲取資料邏輯 ,GraphQL中使用Query來抽象資料的查詢邏輯,分為三種,分別是query(查詢)、mutation(更改)、subscription(訂閱) 。API的介面概括起來有CRUD(建立、獲取、更改、刪除)四類,query可以覆蓋R(獲取)的功能,mutation可以覆蓋(CUD建立、更改、刪除)的功能。
注意: Query特指GraphQL中的查詢(包含三種類型),query指GraphQL中的查詢型別(僅指查詢型別)。
Query
- query(查詢):當獲取資料時,選擇query型別
- mutation(更改): 當嘗試修改資料時,選擇mutation型別
- subscription(訂閱):當希望資料更改時,可以進行訊息推送,使用subscription型別(針對當前的日趨流行的real-time應用提出的)。
以Article為資料模型,分別以REST和GraphQL的角度,編寫CURD的介面
Rest介面
- GET /api/v1/articles/
- GET /api/v1/article/:id/
- POST /api/v1/article/
- DELETE /api/v1/article/:id/
- PATCH /api/v1/article/:id/
GraphQL Query
- query型別
query {
articles():[Article!]!
article(id: Int!): Article!
} - mutation型別
mutation {
createArticle(): Article!
updateArticle(id: Int): Article!
deleteArticle(id: Int): Article!
}
注意:
GraphQL是按照型別來劃分職能的query、mutation、ssubscription,同時必須明確宣告返回的資料型別。
如果實際應用中對於評論列表有real-time 的需求,該如何處理?
在REST中,可以通過長連線,或者通過提供一些帶驗證的獲取長連線URL的介面,比如
POST /api/v1/messages/
之後長連線會將新的資料進行實時推送。在GraphQL中,會以更加宣告式的方式進行宣告,如下:
subscription { updatedArticle() { mutation node { comments: [Comment!]! } } }
此處聲明瞭一個subscription,這個subscription會在有新的Article被建立或者更新時,推送新的資料物件。實際上內部仍然是建立於長連線之上 。
Resolve
上述的描述並未說明如何返回相關操作(query、mutation、subscription)的資料邏輯。所有此處引入一個更核心的概念Resolve(解析函式)
GraphQL中,預設有這樣的約定,Query(包括query、mutation、subscription)和與之對應的Resolve是同名的,比如關於
articles(): [Articles!]!
這個query,它的Resolve的名字必然叫做articles以已經宣告的articles的query為例,解釋下GraphQL的內部工作機制:
Query { articles { id author { name } comments { id desc author } } }
按照如下步驟進行解析:
- 首先進行第一次解析,當前的型別是query 型別,同時Resolver的名字為articles。
- 之後會嘗試使用articles的Resolver獲取解析資料,第一層解析完畢
- 之後對第一層解析的返回值,進行第二層解析,當前articles包含三個子query ,分別是id、author和comments
- id在Author型別中為標量型別,解析結束
- author在articles型別中為物件型別User,嘗試使用User的Resolver獲取資料,當前field解析完畢。
- 之後對第二層解析的返回值,進行第三層解析,當前author還包含一個query,name是標量型別,解析結束
- comments解析同上
- query型別
概括總結GraphQL大體解析流程就是遇見一個Query之後,嘗試使用它的Resolver取值,之後再對返回值進行解析,這個過程是遞迴的,直到所有解析Field型別是Scalar Type(標量型別)為止。整個解析過程可以想象為一個很長的Resolver Chain(解析鏈)。
Resolver本身的宣告在各個語言中是不同的,它代表資料獲取的具體邏輯。它的函式簽名(以golang為例):
func(p graphql.ResolveParams) (interface{}, error) {}
// ResolveParams Params for FieldResolveFn()
type ResolveParams struct {
// Source is the source value
Source interface{}
// Args is a map of arguments for current GraphQL request
Args map[string]interface{}
// Info is a collection of information about the current execution state.
Info ResolveInfo
// Context argument is a context value that is provided to every resolve function within an execution.
// It is commonly
// used to represent an authenticated user, or request-specific caches.
Context context.Context
}
值得注意的是,Resolver內部實現對於GraphQL完全是黑盒狀態。這意味著Resolver如何返回資料、返回什麼樣的資料、從哪裡返回資料,完全取決於Resolver本身。GraphQL在實際使用中常常作為中間層來使用,**資料的獲取通過Resolver來封裝,內部資料獲取的實現可能基於RPC、REST、WS、SQL等多種不同的方式。
GraphQL例子
下面這部分將會展示一個用graphql-go實現的使用者管理的例子,包括獲取全部使用者資訊、獲取指定使用者資訊、修改使用者名稱稱、刪除使用者的功能,以及如何建立列舉型別的功能,完整程式碼在這裡。
生成後的schema檔案內容如下:
type Mutation {
"""[使用者管理] 修改使用者名稱稱"""
changeUserName(
"""使用者ID"""
userId: Int!
"""使用者名稱稱"""
userName: String!
): Boolean
"""[使用者管理] 建立使用者"""
createUser(
"""使用者名稱稱"""
userName: String!
"""使用者郵箱"""
email: String!
"""使用者密碼"""
pwd: String!
"""使用者聯絡方式"""
phone: Int
): Boolean
"""[使用者管理] 刪除使用者"""
deleteUser(
"""使用者ID"""
userId: Int!
): Boolean
}
type Query {
"""[使用者管理] 獲取指定使用者的資訊"""
UserInfo(
"""使用者ID"""
userId: Int!
): userInfo
"""[使用者管理] 獲取全部使用者的資訊"""
UserListInfo: [userInfo]!
}
"""使用者資訊描述"""
type userInfo {
"""使用者email"""
email: String
"""使用者名稱稱"""
name: String
"""使用者手機號"""
phone: Int
"""使用者密碼"""
pwd: String
"""使用者狀態"""
status: UserStatusEnum
"""使用者ID"""
userID: Int
}
"""使用者狀態資訊"""
enum UserStatusEnum {
"""使用者可用"""
EnableUser
"""使用者不可用"""
DisableUser
}
注意
- GraphQL基於golang實現的例子比較少
- GraphQL的schema可以自動生成,具體操作可檢視graphq-cli文件,步驟大致包括npm包的安裝、graphql-cli工具的安裝,配置檔案的更改(此處需要指定服務對外暴露的地址) ,執行
graphql get-schema
命令。
GraphQL API以及Rsolve函式定義
type UserInfo struct {
UserID uint64 `json:"userID"`
Name string `json:"name"`
Email string `json:"email"`
Phone int64 `json:"phone"`
Pwd string `json:"pwd"`
Status model.UserStatusType `json:"status"`
}
//這段內容是如何使用GraphQL定義列舉型別
var UserStatusEnumType = graphql.NewEnum(graphql.EnumConfig{
Name: "UserStatusEnum",
Description: "使用者狀態資訊",
Values: graphql.EnumValueConfigMap{
"EnableUser": &graphql.EnumValueConfig{
Value: model.EnableStatus,
Description: "使用者可用",
},
"DisableUser": &graphql.EnumValueConfig{
Value: model.DisableStatus,
Description: "使用者不可用",
},
},
})
var UserInfoType = graphql.NewObject(graphql.ObjectConfig{
Name: "userInfo",
Description: "使用者資訊描述",
Fields: graphql.Fields{
"userID": &graphql.Field{
Description: "使用者ID",
Type: graphql.Int,
},
"name": &graphql.Field{
Description: "使用者名稱稱",
Type: graphql.String,
},
"email": &graphql.Field{
Description: "使用者email",
Type: graphql.String,
},
"phone": &graphql.Field{
Description: "使用者手機號",
Type: graphql.Int,
},
"pwd": &graphql.Field{
Description: "使用者密碼",
Type: graphql.String,
},
"status": &graphql.Field{
Description: "使用者狀態",
Type: UserStatusEnumType,
},
},
})
query與mutation的定義
var MutationType = graphql.NewObject(graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"createUser": &graphql.Field{
Type: graphql.Boolean,
Description: "[使用者管理] 建立使用者",
Args: graphql.FieldConfigArgument{
"userName": &graphql.ArgumentConfig{
Description: "使用者名稱稱",
Type: graphql.NewNonNull(graphql.String),
},
"email": &graphql.ArgumentConfig{
Description: "使用者郵箱",
Type: graphql.NewNonNull(graphql.String),
},
"pwd": &graphql.ArgumentConfig{
Description: "使用者密碼",
Type: graphql.NewNonNull(graphql.String),
},
"phone": &graphql.ArgumentConfig{
Description: "使用者聯絡方式",
Type: graphql.Int,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
userId, _ := strconv.Atoi(GenerateID())
user := &model.User{
//展示如何解析傳入的引數
Name: p.Args["userName"].(string),
Email: sql.NullString{
String: p.Args["email"].(string),
Valid: true,
},
Pwd: p.Args["pwd"].(string),
Phone: int64(p.Args["phone"].(int)),
UserID: uint64(userId),
Status: int64(model.EnableStatus),
}
if err := model.InsertUser(user); err != nil {
log.WithError(err).Error("[mutaition.createUser] invoke InserUser() failed")
return false, err
}
return true, nil
},
},
},
})
var QueryType = graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"UserListInfo": &graphql.Field{
Description: "[使用者管理] 獲取指定使用者的資訊",
//定義了非空的list型別
Type: graphql.NewNonNull(graphql.NewList(UserInfoType)),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
users, err := model.GetUsers()
if err != nil {
log.WithError(err).Error("[query.UserInfo] invoke InserUser() failed")
return false, err
}
usersList := make([]*UserInfo, 0)
for _, v := range users {
userInfo := new(UserInfo)
userInfo.Name = v.Name
userInfo.Email = v.Email.String
userInfo.Phone = v.Phone
userInfo.Pwd = v.Pwd
userInfo.Status = model.UserStatusType(v.Status)
usersList = append(usersList, userInfo)
}
return usersList, nil
},
},
},
})
注意:
- 此處僅展示了部分例子
- 此處筆者僅列舉了query、mutation型別的定義
如何定義服務main函式
type ServerCfg struct {
Addr string
MysqlAddr string
}
func main() {
//load config info
m := multiconfig.NewWithPath("config.toml")
svrCfg := new(ServerCfg)
m.MustLoad(svrCfg)
//new graphql schema
schema, err := graphql.NewSchema(
graphql.SchemaConfig{
Query: object.QueryType,
Mutation: object.MutationType,
},
)
if err != nil {
log.WithError(err).Error("[main] invoke graphql.NewSchema() failed")
return
}
model.InitSqlxClient(svrCfg.MysqlAddr)
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: true,
})
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
//read user_id from gateway
userIDStr := r.Header.Get("user_id")
if len(userIDStr) > 0 {
userID, err := strconv.Atoi(userIDStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
ctx = context.WithValue(ctx, "ContextUserIDKey", userID)
}
h.ContextHandler(ctx, w, r)
})
log.Fatal(http.ListenAndServe(svrCfg.Addr, nil))
}
展示下GraphQL自帶的GraphiQL除錯工具
筆者初次接觸GraphQL,可能很多理解有誤,歡迎指出。