用Go创建一个基本的GraphQL API的方法

238 阅读4分钟

看起来GraphQL正被越来越多地使用,几年前我曾参加过react生态系统中关于GraphQL的一些首次公开讨论,我并不感到惊讶。

这个概念从多个角度来看都是可行的,包括类似图形的数据、多个分布式团队和高度版本化的API,以及关于类型安全和文档。GraphQL看起来很适合许多不同的应用。

这篇文章的目的不是向你介绍GraphQL的基础知识,而是在一个现实的场景中看到它的作用。当计划将现有的REST API转移到GraphQL时,首先引入一个翻译层是有意义的,这样可以实现平稳过渡。

在这篇文章中,我们将使用jsonplaceholder作为我们将用GraphQL包装的API。Go中有几个graphQL的库,在这个例子中,将使用graphql-gographql-go-handler

jsonplaceholder 我们的目标是从postscomments ,并最终通过ID来获取posts ,如果API消费者希望同时获取评论,则通过GraphQL将评论嵌套到post

让我们开始吧。

实施

首先,我们为PostComment 定义数据模型。

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 。使用这个postid ,我们可以获取它的评论。

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订阅,以及其他一些更高级的使用案例。)

资源