使用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 的查询语言和运行时环境的开源规范。
请用中文将以下内容重述一遍:“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的模式中会变成这样。
查询 {
任务(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的环境。
工作目录/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的最新版本
—
版本: ‘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服务器。
有关每个中间件的详细信息,请参阅 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的设置文件。
—
设置:
旧版:
强制:假
间隔:0秒
架构:
– 名称:应用
路径:.
命令:
安装:
状态:真
方法:go build -o app main.go
运行:
状态:真
方法:./app
观察者:
扩展名:
– go
路径:
– /
忽略的路径:
– .realize
将服务器的启动和迁移命令整理到Makefile中。
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语句。
创建表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 に配置し、パッケージ名を変更する
—
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
//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}
}
删除 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 的配置已經完成。從這裡開始進行每個查詢和變更的實現。
基本上,從這裡開始,我們會進行每個查詢和變更的實現。
-
- 使用中文将以下内容释义,只需提供一种选择:
-
- 在/app/schema/*.graphql中添加/编辑模式
-
- 通过make generate生成代码
- 实现符合创建/修改的接口要求的解析器。
我们将按照这种方式推进实施计划。
为了方便今后执行,我会将”go generate”命令添加到Makefile中。
# 追記
.PHONY: generate
generate:
docker-compose exec app go generate ./...
4. 创建任务模型 (CJ2012’s translation)
接下来,我们将进行数据库设置和任务模型的创建。
首先,将DB相关配置放置在config/db.go文件中。
导入 (
“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 模型。
并且,使用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
…
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文件中进行实现。
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等工具进行测试确认。
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中进行实现。
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 的实现并进行测试。
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。
func (Task) 是否为节点() {} // 追加
下面我們將實現分頁功能。
简单来说,流程如下。
-
- 解码光标以提取密钥,并根据该密钥构建SQL
-
- 发送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;
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等工具中进行操作并确认一下结果。
最新的(按创建日期从新到旧排序)
按照最早的截止日期排序
通过以下步骤,完成了任务管理应用程序的后端实现:
1. 创建模式➝使用go generate生成代码➝实现解析器,这是一种基于GraphQL的模式优先开发的体验。
对于最后的分页部分,需要为每个模型单独创建,稍微有些复杂,但如果能够创建一个通过go generate自动生成的机制,负担就不会太大了。
明天是Climber22先生的前端部分。
请与本次的后端部分一起制作。
我在GraphQL Advent Calendar 2019的第一篇文章中写了关于使用gqlgen运行GraphQL服务器的感想,请务必阅读这篇文章。文章链接:blog.ebiken.dev 上的gqlgen で GraphQLサーバーを運用した感想。