教程介绍
在本教程中,我们将学习如何将 Ent 连接到 GraphQL,并配置 Ent 提供的多种集成功能,例如:
- 为 Ent schema 中定义的节点和边生成 GraphQL schema。
- 自动生成
Query和Mutation解析器,并与 Relay 框架 无缝集成。 - 支持过滤、分页(包括嵌套分页)并兼容 Relay 游标连接规范。
- 高效的字段收集功能,无需数据加载器即可解决 N+1 问题。
- 事务性变更机制,确保故障时的数据一致性。
如果您不熟悉 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 功能:
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.QueryField(),
entgql.Mutations(entgql.MutationCreate()),
}
}
3. 新建名为 ent/entc.go 的 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 命令也会添加到该文件中。
package todo
//go:generate go run -mod=mod ./ent/entc.go
运行 schema 生成
安装并配置 entgql 后,执行代码生成:
go generate .
您会注意到生成了一个名为 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 文件进行配置。让我们在项目根目录添加此文件。通过文件中的注释理解每个配置指令的含义:
# 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 生成命令:
package todo
//go:generate go run -mod=mod ./ent/entc.go
//go:generate go run -mod=mod github.com/99designs/gqlgen
现在,我们已准备好运行 go generate 来触发 ent 和 gqlgen 的代码生成。在项目根目录执行以下命令:
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 Resolver。gqlgen 允许修改生成的 Resolver 并为其添加依赖项。让我们使用 ent.Client 作为依赖项,将以下内容粘贴到 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 服务器:
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 界面:

如果在运行 Playground 时遇到问题,请前往第一节克隆示例仓库。
查询 Todos
如果尝试查询我们的待办事项列表,由于解析器方法尚未实现,将会返回错误。让我们通过替换查询解析器中的 Todos 实现来实现解析器:
func (r *queryResolver) Todos(ctx context.Context) ([]*ent.Todo, error) {
- panic(fmt.Errorf("not implemented"))
+ return r.client.Todo.Query().All(ctx)
}
然后,运行以下 GraphQL 查询应返回空的待办事项列表:
- GraphQL
- 输出结果
query AllTodos {
todos {
id
}
}
{
"data": {
"todos": []
}
}
变更 Todos
如上所示,我们的 GraphQL schema 返回空的待办事项列表。现在让我们创建几个待办事项,但这次我们将通过 GraphQL 进行。幸运的是,Ent 为创建和更新节点及边提供了自动生成的变更功能。
1. 我们首先使用自定义变更扩展 GraphQL schema。新建名为 todo.graphql 的文件并添加我们的 Mutation 类型:
type Mutation {
# 输入和输出类型由 Ent 生成。
createTodo(input: CreateTodoInput!): Todo
}
2. 将自定义 GraphQL schema 添加到 gqlgen.yml 配置中:
schema:
- ent.graphql
- todo.graphql
# ...
3. 运行代码生成:
go generate .
如您所见,gqlgen 为我们生成了一个名为 todo.resolvers.go 的新文件,其中包含 createTodo 解析器。让我们将其连接到 Ent 生成的代码,并让 Ent 处理此变更:
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
}
}
{
"data": {
"createTodo": {
"id": "1",
"text": "Create GraphQL Example",
"createdAt": "2022-09-08T15:20:58.696576+03:00",
"priority": 1,
}
}
}
query {
todos {
id
text
status
}
}
{
"data": {
"todos": [
{
"id": "1",
"text": "Create GraphQL Example",
"status": "IN_PROGRESS"
}
]
}
}
如果运行此示例时遇到问题,请前往第一节克隆示例仓库。
请继续阅读下一节,我们将讲解如何实现 Relay 节点接口,并了解 Ent 如何自动支持此功能。