尽可能以简单明了的方式解释gqlgen教程
首先
作为我的第二家工程师实习公司,我将使用Golang+GraphQL进行开发。
尽管最近对这个领域的理解有所提高,但作为一个只了解REST以外世界的人来说,我还不太明白gqlgen能为我做些什么,它有什么方便之处。所以,我想为那些处于类似境地的人写一篇文章。
前提概念
-
- GraphQLの基本がわかる
- Golangの基本文法がわかる
gqlgen是什么?
从公式中引用
gqlgen 是一个用于构建 GraphQL 服务器的 Go 库,无需任何繁琐过程。
・gqlgen 基于首先定义的模式,通过使用 GraphQL 模式定义语言来定义 API。
・gqlgen 优先确保类型安全性,你在这里不会看到 map[string]interface{}。
・gqlgen 具备代码生成能力,我们生成无聊的部分,让你可以专注快速构建应用程序。
简单来说,这就是说在构建GraphQL服务器时,我们可以使用Golang库以”Schema First”的方式,以保持”类型安全”的前提下,自动生成代码。
目标
按照 gqlgen 教程创建一个简单的 todo 应用程序。我们将在 GraphQL Playround 上确认其运行情况。
我们开始吧!
首先从准备开始。创建工作目录并配置Go开发环境。本次将使用Go语言1.8版本。
mkdir gqlgen_tutorial && cd gqlgen_tutorial
go mod init gqlgen_tutorial
接下来,我们将下载本次重点讨论的gqlgen包以及其相关依赖关系。
go get -u github.com/99designs/gqlgen@v0.17.5
我没有特别的偏好,但这次我会使用最新版本的0.17.5。
然后,您可以使用以下命令创建模板文件。
go run github.com/99designs/gqlgen init

生成的文件说明
-
- graph/generated/generated.go
-
- このファイルはGraphQLサーバーに対するリクエストを解釈しgraph/resolver.goの適切なメソッド呼ぶ役割を果たしています。
-
- graph/model/models_gen.go
-
- schemaで定義したtypeやinputをgolangの構造体に変換したものが定義されます。
-
- graph/schema.resolver.go
- リクエストを元に実際の処理を実装するresolverファイルです。
通过更改上述的三个schema后,执行”go run github.com/99designs/gqlgen generate”将重新生成代码。
-
- graph/resolver.go
-
- ルートとなるresolver構造体が宣言されます。再生成はされません。
-
- graph/schema.graphqls
-
- GraphQLスキーマを定義するファイルです。このファイルをもとに他のファイルが再生成されます。
-
- gqlgen.yml
- gqlgenの設定ファイルです。今回は行いませんがshcemaの分割などの設定もこのファイルで行うことができます。
让我们立即查看graph/schema.graphqls文件。
# GraphQL schema example
#
# https://gqlgen.com/getting-started/
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
type User {
id: ID!
name: String!
}
type Query {
todos: [Todo!]!
}
input NewTodo {
text: String!
userId: String!
}
type Mutation {
createTodo(input: NewTodo!): Todo!
}
有一个我不记得的scheme被生成了。这是gqlgen默认生成的。我打算以这个schema为基础来创建一个简单的todo应用程序。
正如之前所述,gqlgen是基于schema优先的方式生成GraphQL服务器的。无论是在自动生成代码还是实现resolver时,我们都将按照这个基于schema的方式进行实现。如果出现意外错误,我认为从这个schema进行确认是很好的。
接下来我们来看一下graph/model/models_gen.go文件。
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model
type NewTodo struct {
Text string `json:"text"`
UserID string `json:"userId"`
}
type Todo struct {
ID string `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
User *User `json:"user"`
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
在之前查看的schema中,除了定义了Query和Mutation之外的类型被定义为结构体。我们将使用此文件中的结构体来实现后面提到的解析器。
让我们最后看一下graph/schema.resolver.go。
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"gqlgen_tutorial/graph/generated"
"gqlgen_tutorial/graph/model"
)
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
panic(fmt.Errorf("not implemented"))
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
有一些可疑的方法似乎被生成了出来。值得注意的是CreateTodo和Todos。它们在graph/schema.graphqls中被定义。
type Query {
todos: [Todo!]!
}
...省略
type Mutation {
createTodo(input: NewTodo!): Todo!
}
这些方法分别对应这里。在generated.go文件的gqlgen自动生成的代码中,请求GraphQL服务器后,这些方法会被调用以执行相应操作。因此,这些方法扮演了所谓的Controller角色。
目前这些方法的实现部分还是空的,接下来我们会逐步实现其功能。通常情况下数据会被持久化到数据库中,但为了测试方便,我们暂时将数据存储在内存中。
首先,我们需要修改graph/resolver.go文件。
type Resolver struct {
todos []*model.Todo // 追加
}
我想先将todos保留在此Resolver中,因为mutationResolver和queryResolver在schema.resolver.go中进行了定义并包装了该Resolver。
接下来我们将实现resolver。
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
todo := &model.Todo{
Text: input.Text,
ID: fmt.Sprintf("T%d", rand.Int()),
User: &model.User{ID: input.UserID, Name: "user " + input.UserID},
}
r.todos = append(r.todos, todo)
return todo, nil
}
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
return r.todos, nil
}
CreateTodo方法是根据请求信息(input model.NewTodo)创建Todo模型,并将其添加到解析结构体(Resolver struct)中的todos片段中的操作。
在Todos方法中,只是返回了现有Resolver struct中的todos切片。
如果是实际的应用程序,可能会涉及到更复杂的逻辑和数据库操作,但是由于我们希望尽量保持最简单的情况,所以将省略这部分。
因为完成了 resolver 的实现,现在让我们进行操作确认吧!启动 GraphQL 服务器。
go run server.go

mutation {
createTodo(input: { text: "todo", userId: "1" }) {
user {
id
}
text
done
}
}
如果你收到以下的回复,那么表示创建成功!
{
"data": {
"createTodo": {
"user": {
"id": "1"
},
"text": "todo",
"done": false
}
}
}
我们来获取接下来创建的待办事项。
query {
todos {
text
done
user {
name
}
}
}
{
"data": {
"todos": [
{
"text": "todo",
"done": false,
"user": {
"name": "user 1"
}
}
]
}
}
太好了!只需实现resolver,就能如此轻松地创建出GraphQL服务器。
但仅此而已的话,无法享受到GraphQL的一个优点,即可以选择获取的数据。
例如,在当前阶段,无论何时获取todos,都会同时获取到user。当然,
query {
todos {
text
done
}
}
发送类似的请求不会返回用户数据作为响应,但在内部仍在获取用户数据。在使用常见的基于关系型数据库的Web应用程序中,Todo模型仅持有UserID,并且通常需要从数据库中单独获取与之相关的user。在这种情况下,即使无需作为响应返回用户数据,也会执行不必要的SQL操作。
如果保持现状,无法充分享受到GraphQL的好处。通过实现todoResolver,可以解决这个问题。让我们从这里开始修正。
首先需要做的是创建一个名为Todo的新模型作为一个新的模型。在当前状态下,将使用根据schema.graphqls自动生成的Todo模型在models/models_gen.go中。然而,使用这种方式会导致必须在返回CreateTodo mutation的Todo模型中必须包含用户(User)的需求,所以我们需要定义一个新的Todo模型。
请使用以下内容新建graph/models/todo.go文件。
type Todo struct {
ID string
Text string
Done bool
UserID string
}
然后在gqlgen.yml文件中添加以下内容。
models:
Todo:
model: gqlgen_tutorial/graph/model.Todo
这样一来,就可以指定“todo模型将使用gqlgen_tutorial/graph/model.go的Todo结构”。
因为准备完成了,我们来重新生成文件吧。
go run github.com/99designs/gqlgen generate
请查看 graph/model/models_gen.go 文件。注意到 Todo 结构已经消失了,这样刚刚定义的 Todo 模型将会被使用。
那么我们开始实现resolver吧。请打开schema.resolvers.go文件。
由于先前的更改,Todo结构体现在不再保存User,而是保存UserID。
首先,我们要修改CreateTodo函数。
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
todo := &model.Todo{
Text: input.Text,
ID: fmt.Sprintf("T%d", rand.Int()),
UserID: input.UserID, // 修正
}
r.todos = append(r.todos, todo)
return todo, nil
}
我們接下來來介紹之前沒有的用戶方法。
func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
return &model.User{ID: obj.UserID, Name: "user " + obj.UserID}, nil //修正
}
我会进行以上更改。
在这里,我一开始很难理解的是通过mutation的CreateTodo或者query的Todos返回的todo结构,由于todo结构没有schema中的todo类型指定的User字段,所以会调用todoResolver中定义的User方法。
这就完成了。最后再一次。
go run server.go
请执行这个操作,并在GraphQL Playground中进行确认。如果与之前的操作一样,那就完美了。
最终/最后 (zuì
你觉得如何?我知道可能有很多不好理解的地方,但如果能对你有所帮助的话,我会感到荣幸。
我欢迎任何建议、批评和不满!