看起来GraphQL正被越来越多地使用,几年前我曾参加过react生态系统中关于GraphQL的一些首次公开讨论,我并不感到惊讶。
这个概念从多个角度来看都是可行的,包括类似图形的数据、多个分布式团队和高度版本化的API,以及关于类型安全和文档。GraphQL看起来很适合许多不同的应用。
这篇文章的目的不是向你介绍GraphQL的基础知识,而是在一个现实的场景中看到它的作用。当计划将现有的REST API转移到GraphQL时,首先引入一个翻译层是有意义的,这样可以实现平稳过渡。
在这篇文章中,我们将使用jsonplaceholder作为我们将用GraphQL包装的API。Go中有几个graphQL的库,在这个例子中,将使用graphql-go和graphql-go-handler。
jsonplaceholder 我们的目标是从posts 和comments ,并最终通过ID来获取posts ,如果API消费者希望同时获取评论,则通过GraphQL将评论嵌套到post 。
让我们开始吧。
实施
首先,我们为Post 和Comment 定义数据模型。
type Post struct {
UserID int `json:"userId"`
ID int `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
}
type Comment struct {
PostID int `json:"postId"`
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Body string `json:"body"`
}
我们还定义了一个fetchPostByiD(id) 函数,它调用http://jsonplaceholder.typicode.com/posts/${id} ,并将得到的JSON转换为Post 。当然,也有一个fetchCommentsByPostID(post.ID) 辅助函数,它对评论做了同样的工作,从http://jsonplaceholder.typicode.com/posts/${id}/comments 中获取数据,并将其转换为[]Comment 。
然后,我们继续创建我们的graphQL模式。我们首先定义queryType ,它是我们模式的根。
func createQueryType(postType *graphql.Object) graphql.ObjectConfig {
return graphql.ObjectConfig{Name: "QueryType", Fields: graphql.Fields{
"post": &graphql.Field{
Type: postType,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Int),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id := p.Args["id"]
v, _ := id.(int)
log.Printf("fetching post with id: %d", v)
return fetchPostByiD(v)
},
},
}}
}
根查询类型只有一个字段--post 。这个字段由postType 定义,我们很快就会看到它。它只接受一个参数,称为id 。
通过从p.Args 中获取id ,并将其传递给fetchPostsByID ,返回获取和转换的Post ,以及任何错误,来解决职位问题。
接下来,我们定义了postType ,这很有趣。我们基本上只是将数据模型中的post 字段映射为graphQL类型,但我们也添加了comments 字段。评论的Resolve 函数只被调用,如果客户端明确地想要获取它们。
为了解决评论,我们通过使用p.Source 来访问这个查询的 "父",这给我们提供了一个*Post 的实例--所获取的post 。使用这个post 的id ,我们可以获取它的评论。
func createPostType(commentType *graphql.Object) *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{
Name: "Post",
Fields: graphql.Fields{
"userId": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"title": &graphql.Field{
Type: graphql.String,
},
"body": &graphql.Field{
Type: graphql.String,
},
"comments": &graphql.Field{
Type: graphql.NewList(commentType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
post, _ := p.Source.(*Post)
log.Printf("fetching comments of post with id: %d", post.ID)
return fetchCommentsByPostID(post.ID)
},
},
},
})
}
在模式中唯一需要定义的类型是commentType ,这很无聊,因为它只是将数据模型的字段映射到graphQL类型。
func createCommentType() *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{
Name: "Comment",
Fields: graphql.Fields{
"postid": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"name": &graphql.Field{
Type: graphql.String,
},
"email": &graphql.Field{
Type: graphql.String,
},
"body": &graphql.Field{
Type: graphql.String,
},
},
})
}
好了,我们的模式已经定义好了,剩下的事情就是把它放在一起。
我们实例化一个graphQL 模式,并将其传递给graphql-go-handler ,这是一个http中间件,帮助我们处理graphQL查询。然后,我们简单地启动一个http 服务器,将返回的处理程序路由到/graphql 。
这就是它的模样。
func main() {
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(
createQueryType(
createPostType(
createCommentType(),
),
),
),
})
if err != nil {
log.Fatalf("failed to create schema, error: %v", err)
}
handler := gqlhandler.New(&gqlhandler.Config{
Schema: &schema,
})
http.Handle("/graphql", handler)
log.Println("Server started at http://localhost:3000/graphql")
log.Fatal(http.ListenAndServe(":3000", nil))
}
好了,就这样吧!
在启动服务器后,我们可以使用GraphiQL查询具有某个id 的帖子,指定我们感兴趣的字段。
query {
post(id: 5) {
userId
id
body
title
comments {
id
email
name
}
}
}
结果是以下的响应。
{
"data": {
"post": {
"userId": 1,
"id": 5,
"title": "...",
"body": "...",
"comments": [
{
"id": 21,
"email": "...",
"name": "..."
}
]
}
}
}
如果我们在查询中省略了comments ,那么就不会提出获取评论的请求,我们只是得到选定的post 作为响应。
完整的示例代码可以在这里找到。
总结
这个例子展示了如何使用一个薄的Go层将现有的REST API转换为GraphQL。我所使用的库,graphql-go ,工作得很好,提供了坚实的文档和很好的例子,可以遵循。此外,它与我已经熟悉的graphql-js API密切相关,这使得向Go的转换变得更加容易。
肯定有更简洁和更高级的方法来定义这样的模式,但由于入门性质和我对Go中的graphQL的不熟悉,我选择了这个解决方案,它首先关注的是清晰性。
GraphQL似乎是要留下来的,而且是有理由的。我希望在未来的博文中能够涉及到GraphQL订阅,以及其他一些更高级的使用案例。)