使用GraphQL构建任务管理应用-后端部分- [Go + gqlgen]

这是DeNA 20新卒 Advent Calender 2019第21日的文章。

你好,我是ebiken。

我们将分为后端部分和前端部分,使用GraphQL来开发任务管理应用程序。

首先,在后端部分,我们将使用 Go + gqlgen 来实现 GraphQL 服务器。前端部分将由 Climber22 于明天发布,所以请与他一起尝试创建 GraphQL 应用程序。

代码已经在GitHub上公开了。

本次使用的主要语言/库大致如下。

    • Go 1.13.4

 

    • DB

MySQL 8.0.13

ORM

jinzhu/gorm

httpサーバー

labstack/echo

DB マイグレーション

golang-migrate/migrate

サーバーのホットリロード

oxequa/realize

struct のバリデーション

go-playground/validator

ユニークIDの生成

teris-io/shortid

GraphQL 是一种用于 API 的查询语言和运行时环境的开源规范。

undefined

请用中文将以下内容重述一遍:“graphql.org”。

GraphQL 是用于 API 的查询语言,也是用现有数据来满足这些查询的运行时。GraphQL 提供了对 API 数据的完整且易于理解的描述,使客户端能够准确地请求他们所需的数据而不需要多余的信息,使得 API 在时间上更易于演进,并提供了强大的开发者工具。

GraphQL是一种用于API的查询语言和实现。它最初由Facebook开发并开源,目前由GraphQL Foundation推动开发。
通过使用GraphQL,我们可以享受到多种好处,如类型安全的请求、编辑器自动补全、文档生成等周边工具。

    • 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ

 

    GraphQL Advent Calendar 2019

gqlgen 是什么?

请在中国本土中文进行改述:只需提供一种选项: gqldgen.com

gqlgen是一个方便快捷的Go库,用于构建GraphQL服务器。

gqlgen是一個具有Schema First、型安全和代碼生成特性的Go GraphQL服務器庫。
Schema First意味著首先定義模式,然後生成代碼與實施具體處理,可以開發相關的風格。
請參考gqlgen官方網站上與其他使用Go編寫的GraphQL服務器庫進行比較。

引入案例

    • https://tech.mercari.com/entry/2018/10/24/111227

 

    https://tech.mfkessai.co.jp/2018/08/go-gqlgen-graphql/

对于应用程序的规范

这是一个简单的任务管理应用程序。

功能列表

    • タスクの一覧表示

ページネーションも実装する

タスクの並び替え

作成日が新しい順
期限が早い順

タスクの作成
タスクの更新

将这些功能映射到GraphQL的模式中会变成这样。

模式schema.graphql
查询 {
任务(input: 任务输入!, 按照: 任务排序字段!, 分页: 分页输入!): 任务连接!
}

变更 {
创建任务(输入: 创建任务输入!): 任务!
更新任务(输入: 更新任务输入!): 任务!
}

task.graphql
类型 任务 实现节点 {
ID: ID!
标题: String!
注释: String!
已完成: Boolean!
截止时间: 时间!
}
类型 任务边 实现边 {
游标: String!
节点: 任务!
}
类型 任务连接 实现连接 {
页面信息: 页信息!
边: [任务边]!
}

输入 任务输入 {
已完成: Boolean
}

枚举 任务排序字段 {
最新
截止日期
}

输入 创建任务输入 {
标题: String!
注释: String
已完成: Boolean
截止时间: 时间
}

输入 更新任务输入 {
任务ID: ID!
标题: String
注释: String
已完成: Boolean
截止时间: 时间
}

page.graphql
类型 页信息 {
结束游标: String!
是否有下一页: Boolean!
}

接口 连接 {
页面信息: 页信息!
边: [边]!
}
接口 边 {
游标: String!
节点: 节点!
}
接口 节点 {
ID: ID!
}

输入 分页输入 {
第一项: Int
在之后: String
}

在每个功能实现的时候,我们将详细说明。

然后,我们会继续推进实施。

1. 项目的设置

首先,使用Docker和docker-compose来创建Go的环境。

Dockerfile从golang:1.13.4-alpine3.10作为构建环境

工作目录/app

更新apk并禁用缓存
安装git、gcc和musl-dev

复制go.mod和go.sum文件

运行go mod download命令

复制所有文件

在Linux系统上构建AMD64架构的应用(main.go),输出为app

关闭GO111MODULE并获取github.com/oxequa/realize

关闭GO111MODULE,并使用’tags mysql’参数,获取github.com/golang-migrate/migrate/cmd/migrate的最新版本

docker-compose.yml

版本: ‘3.7’服务:
应用程序:
容器名称: graphql-app-backend
构建:
上下文: ./app
目标: 构建
挂载:
– ./app:/app
环境变量:
DB_HOST: db
DB_PORT: 3306
DB_USER: root
DB_PASSWORD: root
DB_NAME: graphql-app-development
端口:
– 3000:3000
依赖:
– db
链接:
– db
终端: true

数据库:
容器名称: graphql-app-db
镜像: mysql:8.0.13
挂载:
– ./db/mysql/data:/var/lib/mysql
– ./db/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
环境变量:
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: graphql-app-development
端口:
– 3306:3306
终端: true

进行Go模块的设置,并使用echo创建一个简单的HTTP服务器。

主要.go使用 echo 创建一个简单的 HTTP 服务器。
有关每个中间件的详细信息,请参阅 echo 的文档
https://echo.labstack.com/middleware

主要.go
包主要

导入 (
“net/http”

“github.com/labstack/echo”
“github.com/labstack/echo/middleware”
)

功能主要() {
e := echo.New()

e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.Use(middleware.Gzip())

e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{os.Getenv(“CORS_ALLOW_ORIGIN”)},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))

e.GET(“/health”, func(c echo.Context) error {
返回 c.NoContent(http.StatusOK)
})

e.HideBanner = true
e.Logger.Fatal(e.Start(“:3000”))
}

为了进行正在开发的热重载,也要配置realize的设置文件。

实现.yml实现.yml

设置:
旧版:
强制:假
间隔:0秒

架构:
– 名称:应用
路径:.
命令:
安装:
状态:真
方法:go build -o app main.go
运行:
状态:真
方法:./app
观察者:
扩展名:
– go
路径:
– /
忽略的路径:
– .realize

将服务器的启动和迁移命令整理到Makefile中。

MakefileDB_HOST=db
DB_PORT=3306
DB_USER=root
DB_PASSWORD=root
DB_NAME=graphql-app-development
DB_CONN=mysql://${DB_USER}:${DB_PASSWORD}@tcp\(${DB_HOST}:${DB_PORT}\)/${DB_NAME}

.PHONY: run
run:
docker-compose up –build -d

.PHONY: start
start:
docker-compose exec app realize start –run

# 创建迁移文件
.PHONY: migrate-create
migrate-create:
docker-compose exec app migrate create -ext sql -dir migrations ${FILENAME}

# 执行迁移
.PHONY: migrate-up
migrate-up:
docker-compose exec app migrate –source file://migrations –database ${DB_CONN} up

# 执行迁移回滚
.PHONY: migrate-down
migrate-down:
docker-compose exec app migrate –source file://migrations –database ${DB_CONN} down 1

我认为文件结构会是这样的样子。

$ tree backend
.
├── .gitignore
├── Makefile
├── README.md
├── app
│   ├── .realize.yaml
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   └── main.go
├── db
│   └── mysql
│       └── my.cnf
└── docker-compose.yml

在backend目录中执行make命令,app服务和db服务会启动,通过执行make start来启动服务器即可。

$ make start
docker-compose exec app realize start --run
[14:28:00][APP] : Watching 9 file/s 6 folder/s
[14:28:00][APP] : Install started
[14:28:01][APP] : Install completed in 0.748 s
[14:28:01][APP] : Running..
[14:28:02][APP] : ⇨ http server started on [::]:3000

创建 tasks 表

接下来,我们将创建一个用于保存任务的tasks表。使用我们之前在Makefile中编写的make migrate-create命令来执行,以创建用于tasks表的迁移文件。

$ FILENAME=create_tasks make migrate-create

运行后,将在migrations目录下创建两个文件:_create_tasks.up.sql和_create_tasks.down.sql。您需要分别在这两个文件中编写up和down的SQL语句。

2019xxxx_create_tasks.up.sql
创建表tasks(
id INT 自增,
identifier varchar(255) 二进制 非空,
title varchar(255) 非空,
notes text 非空,
completed tinyint(1) 非空 默认为0,
due timestamp NULL 默认为NULL,
created_at timestamp 非空,
updated_at timestamp 非空,
deleted_at timestamp NULL 默认为NULL,
PRIMARY KEY (id),
UNIQUE KEY uix_tasks_identifier (identifier)
) ENGINE=InnoDB;2019xxxx_create_tasks.down.sql
如果存在则删除表tasks;

执行Makefile中编写的migrate-up命令以创建表。

$ make migrate-up

如果表格是以这样的方式创建的,那就可以。

mysql> desc tasks;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| identifier | varchar(255) | NO   | UNI | NULL    |                |
| title      | varchar(255) | NO   |     | NULL    |                |
| notes      | text         | NO   |     | NULL    |                |
| completed  | tinyint(1)   | NO   |     | 0       |                |
| due        | timestamp    | YES  |     | NULL    |                |
| created_at | timestamp    | NO   |     | NULL    |                |
| updated_at | timestamp    | NO   |     | NULL    |                |
| deleted_at | timestamp    | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
9 rows in set (0.02 sec)

3. gqlgen的配置

接下来,我们将进行 gqlgen 的配置。基本上与 gqlgen 的教程没有什么区别。

首先,使用gqlgen init命令创建项目模板。

$ docker-compose exec app go run github.com/99designs/gqlgen init

运行后将创建以下文件

gqlgen.yml

gqlgenの設定ファイル

generated.go

GraphQL を実行するランタイム (go generateで更新する)

models_gen.go

不足している model (GraphQL の type, input, enum など) の構造体 (go generateで更新する)

resolver.go

resolver (今後 query, mutation を実装していく部分)

schema.graphql

テンプレートで設定される Todo などの GraphQL スキーマを定義している

server/server.go

サーバーを立ち上げている

我会分别编辑它们。

generated.go ➝ resolver/generated.go に配置し、パッケージを変更する

models_gen.go ➝ model/models_gen.go に配置し、パッケージ名を変更する

创建resolver/model包并进行分割。gqlgen.yml

schema:
– “schema/*.graphql”

exec:
filename: resolver/generated.go
package: resolver

model:
filename: model/models_gen.go
package: model

resolver:
filename: resolver/resolver.go
type: Resolver

将resolver.go文件放置在resolver/resolver.go路径下,并进行以下更改:resolver.go
//go:generate go run github.com/99designs/gqlgen

package resolver

type Resolver struct{}

type queryResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }

func New() *Resolver {
return &Resolver{}
}

func (r *Resolver) Mutation() MutationResolver {
return &mutationResolver{r}
}

func (r *Resolver) Query() QueryResolver {
return &queryResolver{r}
}

删去不必要的 type、query 和 mutation,并在 schema/schema.graphql 中添加空的 Query 和 Mutation。
在 main.go 中添加代码,将 server/server.go 的内容追加进去。
删除 server/server.go 文件,并且在 main.go 中添加代码以启动 GraphQL 服务器。
这里我们使用了 /graphql 作为端点。main.go
package main

import (
“app/resolver”
“net/http”

“github.com/99designs/gqlgen/handler”
“github.com/labstack/echo”
“github.com/labstack/echo/middleware”
)

func main() {
e := echo.New()

e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.Use(middleware.Gzip())

e.GET(“/health”, func(c echo.Context) error {
return c.NoContent(http.StatusOK)
})

e.POST(“/graphql”, func(c echo.Context) error {
config := resolver.Config{
Resolvers: resolver.New(),
}
h := handler.GraphQL(resolver.NewExecutableSchema(config))
h.ServeHTTP(c.Response(), c.Request())

return nil
})

e.HideBanner = true
e.Logger.Fatal(e.Start(“:3000”))
}

只要应用程序的目录结构如下所示,就可以。

$ tree app
.
├── .realize.yaml
├── Dockerfile
├── go.mod
├── go.sum
├── gqlgen.yml
├── main.go
├── migrations
│   ├── <timestamp>_create_tasks.down.sql
│   └── <timestamp>_create_tasks.up.sql
├── model
│   └── models_gen.go
├── resolver
│   ├── generated.go
│   └── resolver.go
└── schema
    └── schema.graphql

gqlgen 的配置已經完成。從這裡開始進行每個查詢和變更的實現。
基本上,從這裡開始,我們會進行每個查詢和變更的實現。

    1. 使用中文将以下内容释义,只需提供一种选择:

 

    1. 在/app/schema/*.graphql中添加/编辑模式

 

    1. 通过make generate生成代码

 

    实现符合创建/修改的接口要求的解析器。

我们将按照这种方式推进实施计划。

为了方便今后执行,我会将”go generate”命令添加到Makefile中。

# 追記
.PHONY: generate
generate:
    docker-compose exec app go generate ./...

4. 创建任务模型 (CJ2012’s translation)

接下来,我们将进行数据库设置和任务模型的创建。

首先,将DB相关配置放置在config/db.go文件中。

config/db.godb.go 包 config

导入 (
“fmt”
“os”

_ “github.com/go-sql-driver/mysql”

“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/mysql”
)

var db *gorm.DB

func InitDB() error {
conn, err := gorm.Open(“mysql”, dbsn())
if err != nil {
return err
}

db = conn.Set(“gorm:auto_update”, false)

return nil
}

func dbsn() string {
return fmt.Sprintf(
“%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local”,
os.Getenv(“DB_USER”),
os.Getenv(“DB_PASSWORD”),
os.Getenv(“DB_HOST”),
os.Getenv(“DB_PORT”),
os.Getenv(“DB_NAME”),
)
}

func DB() *gorm.DB {
return db
}

main.go

func main() {
e := echo.New()

如果发生错误 := config.InitDB(); 错误 != nil {
发生错误(err.Error())
}

接着,在 model/task.go 中创建 Task 模型。

模型/任务.go在模型/任务.go中创建任务模型。
并且,使用GORM的BeforeSave钩子进行结构体验证。
验证器的初始化在模型/模型.go中进行。

模型/任务.go
package 模型

import “time”

type 任务 struct {
ID int
标识符 string `validate:”required,max=255″`
标题 string `validate:”required,max=255″`
笔记 string `validate:”max=65535″`
已完成 bool
到期时间 *time.Time
创建时间 time.Time
更新时间 time.Time
删除时间 *time.Time
}

func (t *任务) BeforeSave() error {
return 验证器.Struct(t)
}

模型/模型.go
package 模型

import (
v “gopkg.in/go-playground/validator.v9”
)

var 验证器 *v.Validate

func init() {
验证器 = v.New()
}

接下来,我们将对Tasktype进行模式添加,并将其与创建的模型进行关联。

首先,创建schema/task.graphql。

type Task {
  id: ID!
  title: String!
  notes: String!
  completed: Boolean!
  due: Time
}

由于Time型使用了gqlgen的内置标量类型,所以需要在schema.graphql中进行追加。

    https://gqlgen.com/reference/scalars/#time
# 追記
scalar Time

在gqlgen.yml中进行配置可以将GraphQL的type和model进行关联。
由于希望在Task的id中使用全局唯一的值,而不是在模型的id中,因此希望返回一个标识符。
因此,我们将指定resolver: true,并将解析id的值交给resolver来处理。

将以下内容使用中文进行改写,只需提供一个选项:将以下内容追加到gqlgen.yml文件中:

gqlgen.yml

models:
Task:
model: app/model.Task
fields:
id:
resolver: true

如果进行了更改,请运行 `make generate` 来更新 `resolver/generated.go`。之后,在 `resolver/task.go` 中实现 `TaskResolver` 接口。

任务解析器
任务解析器
包解析器import (
“app/model”
“context”
)

type 任务解析器结构体{ *解析器结构体 }

func (r *解析器结构体) 任务() 任务解析器 {
return &任务解析器结构体{r}
}

func (r *任务解析器结构体) ID(ctx context.Context, obj *model.Task) (string, error) {
if obj == nil {
return “”, nil
}

return obj.标识符, nil
}

5. 实施任务创建功能(createTask mutation)

接下来,我们将实现任务创建功能(createTask mutation)。

首先,我们创建突变模式。

...
type Mutation {
  createTask(input: CreateTaskInput!): Task! # 追記
}
# 追記
input CreateTaskInput {
  title: String!
  notes: String
  completed: Boolean
  due: Time
}

可以不使用input参数来定义createTask(title: String!, …) : Task! ,但是将其作为input的方式会提高可读性,并且在生成代码时会生成input对象和结构体,使处理更方便。

由于已经创建了模式,接下来将生成代码。

$ make generate

由于MutationResolver接口中添加了createTask函数,因此我们需要在resolver/resolver.go文件中进行实现。

resolver.go
func (r *mutationResolver) CreateTask(ctx context.Context, input model.CreateTaskInput) (*model.Task, error) {
db := config.DB()id, err := config.ShortID().Generate()
if err != nil {
return &model.Task{}, err
}

task := model.Task{
Identifier: id,
Title: input.Title,
Due: input.Due,
}
if input.Notes != nil {
task.Notes = *input.Notes
}
if input.Completed != nil {
task.Completed = *input.Completed
}

if err := db.Create(&task).Error; err != nil {
return &model.Task{}, err
}

return &task, nil
}
解析器/resolver.go

func (r *mutationResolver) CreateTask(ctx context.Context, input model.CreateTaskInput) (*model.Task, error) {
db := config.DB()

id, err := config.ShortID().Generate()
if err != nil {
return &model.Task{}, err
}

task := model.Task{
Identifier: id,
Title: input.Title,
Due: input.Due,
}
if input.Notes != nil {
task.Notes = *input.Notes
}
if input.Completed != nil {
task.Completed = *input.Completed
}

if err := db.Create(&task).Error; err != nil {
return &model.Task{}, err
}

return &task, nil
}

现在已经完成了createTask mutation的实现,请使用GraphiQL或graphql-playground等工具进行测试确认。

createTask mutation

6. 实现任务更新功能(updateTask mutation)

接下来我们将实现任务更新功能(updateTask mutation)。

与createTask类似,首先创建架构。

...
type Mutation {
  ...
  updateTask(input: UpdateTaskInput!): Task! # 追記
}
# 追記
input UpdateTaskInput {
  taskID: ID!
  title: String
  notes: String
  completed: Boolean
  due: Time
}

因为已经创建了模式,所以接下来需要生成代码。

$ make generate

由于MutationResolver接口中添加了updateTask方法,所以会在resolver/resolver.go中进行实现。

resolver/resolver.go与CreateTask不同的是,将map[string]interface{}类型的参数传递给Updates(),这是因为GORM的Updates()方法在指定零值时不会进行更新。如果有更好的写法,请告诉我。

http://jinzhu.me/gorm/crud.html#update

resolver.go
func (r *mutationResolver) UpdateTask(ctx context.Context, input model.UpdateTaskInput) (*model.Task, error) {
db := config.DB()

var task model.Task
if err := db.Where(“identifier = ?”, input.TaskID).First(&task).Error; err != nil {
return &model.Task{}, err
}

params := map[string]interface{}{}
if input.Title != nil {
params[“title”] = *input.Title
}
if input.Notes != nil {
params[“notes”] = *input.Notes
}
if input.Completed != nil {
params[“completed”] = *input.Completed
}
if input.Due == nil {
params[“due”] = nil
} else {
params[“due”] = *input.Due
}

if err := db.Model(&task).Updates(params).Error; err != nil {
return &model.Task{}, err
}

return &task, nil
}

请使用GraphiQL或graphql-playground等工具来确认已经完成了 updateTask mutation 的实现并进行测试。

updataTask mutation

7. 实现任务列表显示(任务查询)

接下来我们将实现任务列表显示功能(tasks query)。

在一覽顯示的功能中,我們將實現分頁功能。當使用GraphQL進行分頁時,通常會實現基於游標的分頁,也稱為relay樣式分頁。在本次中,我們只會實現從前往後讀取(使用first和after的方式)。

关于 relay 风格的分页,我参考了以下的资料:
– relay 风格分页的规范
– 让我们来分页吧!- 在 GraphQL 中尝试分页!- Qiita
– 关于 Relay 光标连接的规范和实现方法- Qiita

首先从模式定义开始。

type Query {
  tasks(input: TasksInput!, orderBy: TaskOrderFields!,  page: PaginationInput!): TaskConnection!
}
...
type Task implements Node { # implements Node を追記
...
}
# 追記
type TaskEdge implements Edge {
  cursor: String!
  node: Task!
}

type TaskConnection implements Connection {
  pageInfo: PageInfo!
  edges: [TaskEdge]!
}

input TasksInput {
  completed: Boolean
}

enum TaskOrderFields {
  LATEST
  DUE
}
...
type PageInfo {
  endCursor: String!
  hasNextPage: Boolean!
}

interface Connection {
  pageInfo: PageInfo!
  edges: [Edge]!
}

interface Edge {
  cursor: String!
  node: Node!
}

interface Node {
  id: ID!
}

input PaginationInput {
  first: Int
  after: String
}

为了使页面分页部分通用化,我们使用了接口。
此外,虽然还有其他方法,但为了共享包含分页的查询,我们创建了名为PaginationInput的类型输入。
(因为这次只有Task模型,所以意义不大)。

一旦更改后,更新”resolver/generated.go”。

$ make generate

首先,需要将Task模型更改为实现Node。

模型/任务.go任务.go
func (Task) 是否为节点() {} // 追加

下面我們將實現分頁功能。

简单来说,流程如下。

    1. 解码光标以提取密钥,并根据该密钥构建SQL

 

    1. 发送SQL

 

    将结果数组转换为连接

我们将根据创建日期的新旧顺序和期限的远近顺序进行排序实现,但不能简单地通过指定where/order来对每个排序列进行排序。

在第一种情况下,按照id降序排序。由于id是唯一的值,因此游标包含id,可以轻松进行游标分页。
然而,在第二种情况下,可以按照created_at升序进行排序,但由于它不是唯一的值,因此游标分页可能无法正常工作。
有几种实现方法,但这次我们通过调整游标的格式和SQL来实现使用非唯一值进行排序的游标分页。

我正在构建一个包含以下具体指针和SQL语句的系统。

按照id(唯一标识)降序排序

    • カーソル

task:5

SQL

SELECT * FROM tasks WHERE id > 5 ORDER BY id DESC;

按照created_at(非唯一字段)升序排序

    • カーソル

task:5:created_at:123456 (1234..は unix timestamp)

SQL

SELECT * FROM tasks WHERE (UNIX_TIMESTAMP(created_at) < 123456) OR (UNIX_TIMESTAMP(created_at) = 123456 AND id < 5) ORDER BY created_at IS NULL ASC, id ASC;

解析器/resolver.goresolver/resolver.go
func (r *queryResolver) Tasks(ctx context.Context, input model.TasksInput, orderBy model.TaskOrderFields, page model.PaginationInput) (*model.TaskConnection, error) {
db := config.DB()

if input.Completed != nil {
db = db.Where(“completed = ?”, *input.Completed)
}

var err error

switch orderBy {
case model.TaskOrderFieldsLatest:
db, err = pageDB(db, “id”, desc, page)
if err != nil {
return &model.TaskConnection{PageInfo: &model.PageInfo{}}, err
}

var tasks []*model.Task
if err := db.Find(&tasks).Error; err != nil {
return &model.TaskConnection{PageInfo: &model.PageInfo{}}, err
}

return convertToConnection(tasks, orderBy, page), nil
case model.TaskOrderFieldsDue:
db, err = pageDB(db, “UNIX_TIMESTAMP(due)”, asc, page)
if err != nil {
return &model.TaskConnection{PageInfo: &model.PageInfo{}}, err
}

var tasks []*model.Task
if err := db.Find(&tasks).Error; err != nil {
return &model.TaskConnection{PageInfo: &model.PageInfo{}}, err
}

return convertToConnection(tasks, orderBy, page), nil
default:
return &model.TaskConnection{PageInfo: &model.PageInfo{}}, errors.New(“invalid order by”)
}
}

resolver/page.go
package resolver

import (
“app/model”
“encoding/base64”
“errors”
“fmt”
“strconv”
“strings”

“github.com/jinzhu/gorm”
)

type direction string

var (
// 今回は不要 asc direction = “asc”
desc direction = “desc”
)

func pageDB(db *gorm.DB, col string, dir direction, page model.PaginationInput) (*gorm.DB, error) {
var limit int
if page.First == nil {
limit = 11
} else {
limit = *page.First + 1
}

if page.After != nil {
resource1, resource2, err := decodeCursor(*page.After)
if err != nil {
return db, err
}

if resource2 != nil {
switch dir {
case asc:
db = db.Where(
fmt.Sprintf(“(%s > ?) OR (%s = ? AND id > ?)”, col, col),
resource1.ID,
resource1.ID, resource2.ID,
)
case desc:
db = db.Where(
fmt.Sprintf(“(%s < ?) OR (%s = ? AND id < ?)”, col, col), resource1.ID, resource1.ID, resource2.ID, ) } } else { switch dir { case asc: db = db.Where(fmt.Sprintf(“%s > ?”, col), resource1.ID)
case desc:
db = db.Where(fmt.Sprintf(“%s < ?”, col), resource1.ID) } } } switch dir { case asc: db = db.Order(fmt.Sprintf(“%s IS NULL ASC, id ASC”, col)) case desc: db = db.Order(fmt.Sprintf(“%s DESC, id DESC”, col)) } return db.Limit(limit), nil } type cursorResource struct { Name string ID int } func createCursor(first cursorResource, second *cursorResource) string { var cursor []byte if second != nil { cursor = []byte(fmt.Sprintf(“%s:%d:%s:%d”, first.Name, first.ID, second.Name, second.ID)) } else { cursor = []byte(fmt.Sprintf(“%s:%d”, first.Name, first.ID)) } return base64.StdEncoding.EncodeToString(cursor) } func decodeCursor(cursor string) (cursorResource, *cursorResource, error) { bytes, err := base64.StdEncoding.DecodeString(cursor) if err != nil { return cursorResource{}, nil, err } vals := strings.Split(string(bytes), “:”) switch len(vals) { case 2: id, err := strconv.Atoi(vals[1]) if err != nil { return cursorResource{}, nil, errors.New(“无效游标”) } return cursorResource{Name: vals[0], ID: id}, nil, nil case 4: id, err := strconv.Atoi(vals[1]) if err != nil { return cursorResource{}, nil, errors.New(“无效游标”) } id2, err := strconv.Atoi(vals[3]) if err != nil { return cursorResource{}, nil, errors.New(“无效游标”) } return cursorResource{ Name: vals[0], ID: id, }, &cursorResource{ Name: vals[2], ID: id2, }, nil default: return cursorResource{}, nil, errors.New(“无效游标”) } } func convertToConnection(tasks []*model.Task, orderBy model.TaskOrderFields, page model.PaginationInput) *model.TaskConnection { if len(tasks) == 0 { return &model.TaskConnection{PageInfo: &model.PageInfo{}} } pageInfo := model.PageInfo{} if page.First != nil { if len(tasks) >= *page.First+1 {
pageInfo.HasNextPage = true
tasks = tasks[:len(tasks)-1]
}
}

switch orderBy {
case model.TaskOrderFieldsLatest:
taskEdges := make([]*model.TaskEdge, len(tasks))

for i, task := range tasks {
cursor := createCursor(
cursorResource{Name: “task”, ID: task.ID},
nil,
)
taskEdges[i] = &model.TaskEdge{
Cursor: cursor,
Node: task,
}
}

pageInfo.EndCursor = taskEdges[len(taskEdges)-1].Cursor

return &model.TaskConnection{PageInfo: &pageInfo, Edges: taskEdges}
case model.TaskOrderFieldsDue:
taskEdges := make([]*model.TaskEdge, 0, len(tasks))

for _, task := range tasks {
if task.Due == nil {
pageInfo.HasNextPage = false
return &model.TaskConnection{PageInfo: &pageInfo, Edges: taskEdges}
}

cursor := createCursor(
cursorResource{Name: “task”, ID: int(task.Due.Unix())},
&cursorResource{Name: “due”, ID: task.ID},
)

taskEdges = append(taskEdges, &model.TaskEdge{
Cursor: cursor,
Node: task,
})
}

pageInfo.EndCursor = taskEdges[len(taskEdges)-1].Cursor

return &model.TaskConnection{PageInfo: &pageInfo, Edges: taskEdges}
}

return &model.TaskConnection{PageInfo: &model.PageInfo{}}
}

既然已经完成了任务查询的实现,那就请在GraphiQL或graphql-playground等工具中进行操作并确认一下结果。

最新的(按创建日期从新到旧排序)

tasks order by latest query

按照最早的截止日期排序

tasks order by due query

通过以下步骤,完成了任务管理应用程序的后端实现:
1. 创建模式➝使用go generate生成代码➝实现解析器,这是一种基于GraphQL的模式优先开发的体验。
对于最后的分页部分,需要为每个模型单独创建,稍微有些复杂,但如果能够创建一个通过go generate自动生成的机制,负担就不会太大了。

明天是Climber22先生的前端部分。
请与本次的后端部分一起制作。


我在GraphQL Advent Calendar 2019的第一篇文章中写了关于使用gqlgen运行GraphQL服务器的感想,请务必阅读这篇文章。文章链接:blog.ebiken.dev 上的gqlgen で GraphQLサーバーを運用した感想。

广告
将在 10 秒后关闭
bannerAds