尽可能以简单明了的方式解释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ì

你觉得如何?我知道可能有很多不好理解的地方,但如果能对你有所帮助的话,我会感到荣幸。
我欢迎任何建议、批评和不满!

bannerAds