REST API的使用方式是,server定义一系列的接口,client调用自己需要的接口,获取目标数据进行整合。REST API开发中遇到的问题:
GraphQL解决的问题:
GraphQL官网给出定义:GraphQL既是一种用于API的查询语言 也是一个满足你数据查询的运行时 。GraphQL对你的API中的数据提供了一套易于理解的完整描述 ,使得客户端能够准确地获得它需要的数据 ,而且没有任何冗余,也让API更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
ask exactly what you want
在GraphQL中,通过定义一张Schema和声明一些Type来达到上述描述的功能,需要学习:
对于数据模型的抽象是通过Type来描述的,每一个Type有若干Field组成,每个Field又分别指向某个Type。
GraphQL的Type简单可以分为两种,一种是scalar type(标量类型) ,另一种是object type(对象类型)。
GraphQL中的内建的标量包含,String、Int、Float、Boolean、Enum,除此之外,GraphQL中可以通过scalar声明一个新的标量 ,比如:
仅有标量是不够抽象一些复杂的数据模型,这时需要使用对象类型。通过对象类型来构建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
}
类型修饰符,当前的类型修饰符有两种,分别是List和Required ,语法分别为[Type]和[Type!],两者可以组合:
schema用来描述对于接口获取数据逻辑 ,GraphQL中使用Query来抽象数据的查询逻辑,分为三种,分别是query(查询)、mutation(更改)、subscription(订阅) 。API的接口概括起来有CRUD(创建、获取、更改、删除)四类,query可以覆盖R(获取)的功能,mutation可以覆盖(CUD创建、更改、删除)的功能。
注意: Query特指GraphQL中的查询(包含三种类型),query指GraphQL中的查询类型(仅指查询类型)。
以Article为数据模型,分别以REST和GraphQL的角度,编写CURD的接口
Rest接口
GraphQL Query
注意:
GraphQL是按照类型来划分职能的query、mutation、ssubscription,同时必须明确声明返回的数据类型。
如果实际应用中对于评论列表有real-time 的需求,该如何处理?
在REST中,可以通过长连接,或者通过提供一些带验证的获取长连接URL的接口,比如POST /api/v1/messages/
之后长连接会将新的数据进行实时推送。
在GraphQL中,会以更加声明式的方式进行声明,如下:
subscription {
updatedArticle() {
mutation
node {
comments: [Comment!]!
}
}
}
此处声明了一个subscription,这个subscription会在有新的Article被创建或者更新时,推送新的数据对象。实际上内部仍然是建立于长连接之上 。
上述的描述并未说明如何返回相关操作(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
}
}
}
按照如下步骤进行解析:
概括总结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-go实现的用户管理的例子,包括获取全部用户信息、获取指定用户信息、修改用户名称、删除用户的功能,以及如何创建枚举类型的功能,完整代码在这里。
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 get-schema
命令。
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,
},
},
})
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
},
},
},
})
注意:
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,可能很多理解有误,欢迎指出。