GraphQL背景
Go語言的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 API能夠更好的利用你的現有數據和代碼。GraphQL 引擎已經有多種語言實現,GraphQL不限于某一特定數據庫,可以使用已經存在的數據、代碼、甚至可以連接第三方的APIs。
- API 演進無需劃分版本: 給GraphQL API添加字段和類型而無需影響現有查詢。老舊字段可以廢棄,從工具中隱藏。
什么是GraphQL
GraphQL官網給出定義:GraphQL既是一種用于API的查詢語言 也是一個滿足你數據查詢的運行時 。GraphQL對你的API中的數據提供了一套易于理解的完整描述 ,使得客戶端能夠準確地獲得它需要的數據 ,而且沒有任何冗余,也讓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 { articles():[Article!]! article(id: Int!): Article! }
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、mut tion、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解析同上
概括總結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,可能很多理解有誤,歡迎指出。