跳到主要内容

教程介绍

在本教程中,我们将学习如何将 Ent 连接到 GraphQL,并配置 Ent 提供的多种集成功能,例如:

  1. 为 Ent schema 中定义的节点和边生成 GraphQL schema。
  2. 自动生成 QueryMutation 解析器,并与 Relay 框架 无缝集成。
  3. 支持过滤、分页(包括嵌套分页)并兼容 Relay 游标连接规范
  4. 高效的字段收集功能,无需数据加载器即可解决 N+1 问题。
  5. 事务性变更机制,确保故障时的数据一致性。

如果您不熟悉 GraphQL,建议在阅读本教程前先学习其入门指南

克隆代码(可选)

本教程的代码托管在 github.com/a8m/ent-graphql-example,每个步骤都有对应的 Git 标签。如果想跳过基础设置直接开始使用 GraphQL 服务器的初始版本,可以通过以下命令克隆仓库:

git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
go run ./cmd/todo

基础设置

本教程将承接上一个教程(已完成 Todo-list schema 构建)。我们首先安装 contrib/entgql Ent 扩展,并用它生成我们的第一个 schema。然后安装并配置 99designs/gqlgen 框架来构建 GraphQL 服务器,同时探索 Ent 提供的官方集成功能。

安装并配置 entgql

1. 安装 entgql

go get entgo.io/contrib/entgql@master

2.Todo schema 中添加以下注解以启用 Query 和(创建)Mutation 功能:

ent/schema/todo.go
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.QueryField(),
entgql.Mutations(entgql.MutationCreate()),
}
}

3. 新建名为 ent/entc.go 的 Go 文件,并粘贴以下内容:

ent/entc.go
//go:build ignore

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/contrib/entgql"
)

func main() {
ex, err := entgql.NewExtension(
// 指示 Ent 为 Ent schema 生成 GraphQL schema,
// 输出到名为 ent.graphql 的文件中。
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath("ent.graphql"),
)
if err != nil {
log.Fatalf("创建 entgql 扩展失败: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("运行 ent 代码生成失败: %v", err)
}
}
备注

ent/entc.go 通过构建标签被忽略,它通过 generate.go 文件由 go generate 命令执行。

4. 删除 ent/generate.go 文件,在项目根目录新建一个包含以下内容的文件。后续步骤中,gqlgen 命令也会添加到该文件中。

generate.go
package todo

//go:generate go run -mod=mod ./ent/entc.go

运行 schema 生成

安装并配置 entgql 后,执行代码生成:

go generate .

您会注意到生成了一个名为 ent.graphql 的新文件:

ent.graphql
directive @goField(forceResolver: Boolean, name: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
"""
定义 Relay 游标类型:
https://relay.dev/graphql/connections.htm#sec-Cursor
"""
scalar Cursor
"""
具有 ID 的对象。
遵循 [Relay 全局对象标识规范](https://relay.dev/graphql/objectidentification.htm)
"""
interface Node @goModel(model: "todo/ent.Noder") {
"""对象的 ID。"""
id: ID!
}

# ...

安装并配置 gqlgen

1. 安装 99designs/gqlgen

go get github.com/99designs/gqlgen

2. gqlgen 包可通过当前目录自动加载的 gqlgen.yml 文件进行配置。让我们在项目根目录添加此文件。通过文件中的注释理解每个配置指令的含义:

gqlgen.yml
# schema 指示 gqlgen GraphQL schema 的位置。
schema:
- ent.graphql

# resolver 指示解析器实现的存放位置。
resolver:
layout: follow-schema
dir: .

# gqlgen 将在这些 Go 包中搜索 schema 中的类型名称
# 如果匹配则使用,否则会生成新类型。

# autobind 指示 gqlgen 在指定包中搜索 GraphQL schema 中的类型名称。
# 如果匹配则使用,否则会生成新类型。
autobind:
- todo/ent
- todo/ent/todo

# 此部分声明 GraphQL 与 Go 类型系统之间的映射关系。
models:
# 将 ID 字段定义为 Go 的 'int' 类型。
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
- todo/ent.Noder

3. 编辑 ent/entc.go 让 Ent 了解 gqlgen 配置:

//go:build ignore

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/contrib/entgql"
)

func main() {
ex, err := entgql.NewExtension(
// 指示 Ent 为 Ent schema 生成 GraphQL schema,
// 输出到名为 ent.graphql 的文件中。
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath("ent.graphql"),
entgql.WithConfigPath("gqlgen.yml"),
)
if err != nil {
log.Fatalf("创建 entgql 扩展失败: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("运行 ent 代码生成失败: %v", err)
}
}

4.generate.go 文件中添加 gqlgen 生成命令:

generate.go
package todo

//go:generate go run -mod=mod ./ent/entc.go
//go:generate go run -mod=mod github.com/99designs/gqlgen

现在,我们已准备好运行 go generate 来触发 entgqlgen 的代码生成。在项目根目录执行以下命令:

go generate .

您可能已注意到 gqlgen 生成了一些文件:

tree -L 1
.
├── ent/
├── ent.graphql
├── ent.resolvers.go
├── example_test.go
├── generate.go
├── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
└── resolver.go

基础服务器

在构建 GraphQL 服务器之前,我们需要设置在 resolver.go 中定义的主 schema Resolvergqlgen 允许修改生成的 Resolver 并为其添加依赖项。让我们使用 ent.Client 作为依赖项,将以下内容粘贴到 resolver.go 中:

resolver.go
package todo

import (
"todo/ent"

"github.com/99designs/gqlgen/graphql"
)

// Resolver 是解析器根节点。
type Resolver struct{ client *ent.Client }

// NewSchema 创建可执行的 graphql schema。
func NewSchema(client *ent.Client) graphql.ExecutableSchema {
return NewExecutableSchema(Config{
Resolvers: &Resolver{client},
})
}

设置主解析器后,我们新建一个目录 cmd/todo 和一个包含以下代码的 main.go 文件来设置 GraphQL 服务器:

cmd/todo/main.go

package main

import (
"context"
"log"
"net/http"

"todo"
"todo/ent"
"todo/ent/migrate"

"entgo.io/ent/dialect"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"

_ "github.com/mattn/go-sqlite3"
)

func main() {
// 创建 ent.Client 并运行 schema 迁移。
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal("打开 ent 客户端失败", err)
}
if err := client.Schema.Create(
context.Background(),
migrate.WithGlobalUniqueID(true),
); err != nil {
log.Fatal("打开 ent 客户端失败", err)
}

// 配置服务器并开始监听 :8081 端口。
srv := handler.NewDefaultServer(todo.NewSchema(client))
http.Handle("/",
playground.Handler("Todo", "/query"),
)
http.Handle("/query", srv)
log.Println("正在监听 :8081 端口")
if err := http.ListenAndServe(":8081", nil); err != nil {
log.Fatal("HTTP 服务器终止", err)
}
}

使用以下命令运行服务器,并打开 localhost:8081

go run ./cmd/todo

您应该会看到交互式 Playground 界面:

tutorial-todo-playground

如果在运行 Playground 时遇到问题,请前往第一节克隆示例仓库。

查询 Todos

如果尝试查询我们的待办事项列表,由于解析器方法尚未实现,将会返回错误。让我们通过替换查询解析器中的 Todos 实现来实现解析器:

ent.resolvers.go
func (r *queryResolver) Todos(ctx context.Context) ([]*ent.Todo, error) {
- panic(fmt.Errorf("not implemented"))
+ return r.client.Todo.Query().All(ctx)
}

然后,运行以下 GraphQL 查询应返回空的待办事项列表:

query AllTodos {
todos {
id
}
}

变更 Todos

如上所示,我们的 GraphQL schema 返回空的待办事项列表。现在让我们创建几个待办事项,但这次我们将通过 GraphQL 进行。幸运的是,Ent 为创建和更新节点及边提供了自动生成的变更功能。

1. 我们首先使用自定义变更扩展 GraphQL schema。新建名为 todo.graphql 的文件并添加我们的 Mutation 类型:

todo.graphql
type Mutation {
# 输入和输出类型由 Ent 生成。
createTodo(input: CreateTodoInput!): Todo
}

2. 将自定义 GraphQL schema 添加到 gqlgen.yml 配置中:

gqlgen.yml
schema:
- ent.graphql
- todo.graphql
# ...

3. 运行代码生成:

go generate .

如您所见,gqlgen 为我们生成了一个名为 todo.resolvers.go 的新文件,其中包含 createTodo 解析器。让我们将其连接到 Ent 生成的代码,并让 Ent 处理此变更:

todo.resolvers.go
func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
- panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
+ return r.client.Todo.Create().SetInput(input).Save(ctx)
}

4. 重新运行 go run ./cmd/todo 并前往 Playground:

演示

至此,我们已经可以创建待办事项并进行查询:

mutation CreateTodo {
createTodo(input: {text: "Create GraphQL Example", status: IN_PROGRESS, priority: 1}) {
id
text
createdAt
priority
}
}

如果运行此示例时遇到问题,请前往第一节克隆示例仓库。


请继续阅读下一节,我们将讲解如何实现 Relay 节点接口,并了解 Ent 如何自动支持此功能。