跳到主要内容

介绍

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

  1. 为 Ent 模式中定义的节点和边生成 GraphQL 模式。
  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

基础安装

本教程在上一步(创建待办事项清单模式)完成之后进行。我们以安装 Ent 扩展 contrib/entgql 作为开始,并使用它生成我们的第一个模式。 然后安装并配置 99designs/gqlgen 框架来创建 GraphQL 服务器,并探索 Ent 官方提供的集成方案。

安装并配置 entgql

1. 安装 entgql:

go get entgo.io/contrib/entgql@master

2. 添加以下注解到 Todo 模式启动 QueryMutation(创建)能力:

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

3. 创建一个名为 ent/entc.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(
// Tell Ent to generate a GraphQL schema for
// the Ent schema in a file named ent.graphql.
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath("ent.graphql"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
备注

使用创建(build)标签时 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

运行模式创建

在运行并配置 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
"""
Define a Relay Cursor type:
https://relay.dev/graphql/connections.htm#sec-Cursor
"""
scalar Cursor
"""
An object with an ID.
Follows the [Relay Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm)
"""
interface Node @goModel(model: "todo/ent.Noder") {
"""The id of the object."""
id: ID!
}

# ...

安装并配置 gqlgen

1. 安装 99designs/gqlgen:

go get github.com/99designs/gqlgen

2. gqlgen 包可以通过使用 gqlgen.yml 文件进行配置,该文件会在当前目录中自动加载。 让我们在项目根目录中添加这个文件。通过文件中的备注理解每个配置的意义:

gqlgen.yml
# schema tells gqlgen where the GraphQL schema is located.
schema:
- ent.graphql

# resolver reports where the resolver implementations go.
resolver:
layout: follow-schema
dir: .

# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.

# autobind tells gqngen to search for any type names in the GraphQL schema in the
# provided package. If they match it will use them, otherwise it will generate new.
autobind:
- todo/ent
- todo/ent/todo

# This section declares type mapping between the GraphQL and Go type systems.
models:
# Defines the ID field as 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(
// Tell Ent to generate a GraphQL schema for
// the Ent schema in a file named ent.graphql.
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath("ent.graphql"),
entgql.WithConfigPath("gqlgen.yml"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

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

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 中定义的主要模式 Resolvergqlgen 允许变更生成的 Resolver 并添加它的依赖。 让我们在 resolver.go 中粘贴以下内容将 ent.Client 作为依赖项:

resolver.go
package todo

import (
"todo/ent"

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

// Resolver is the resolver root.
type Resolver struct{ client *ent.Client }

// NewSchema creates a graphql executable 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() {
// Create ent.Client and run the schema migration.
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal("opening ent client", err)
}
if err := client.Schema.Create(
context.Background(),
migrate.WithGlobalUniqueID(true),
); err != nil {
log.Fatal("opening ent client", err)
}

// Configure the server and start listening on :8081.
srv := handler.NewDefaultServer(todo.NewSchema(client))
http.Handle("/",
playground.Handler("Todo", "/query"),
)
http.Handle("/query", srv)
log.Println("listening on :8081")
if err := http.ListenAndServe(":8081", nil); err != nil {
log.Fatal("http server terminated", err)
}
}

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

go run ./cmd/todo

你应该看到可互操作的 playground:

tutorial-todo-playground

如果你在运行 playground 遇到了麻烦,到 第一部分 克隆示例代码仓库。

查询待办事项

如果我们尝试查询待办事项列表,我们会收到错误,因为解析器方法还未实现。 让我们在查询解析器中替换 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
}
}

突变(修改/创建)待办事项

如上述所见,GraphQL 模式返回空的待办事项列表。 让我们创建几条待办事项,但这次我们通过 GraphQL 实现它。 幸运的是,Ent 为创建和更新节点和边提供了自动生成的突变。

1. 我们通过自定义突变扩展 GraphQL 模式进行开始。 让我们创建一个名为 todo.graphql 的新文件并添加 Mutation 类型:

todo.graphql
type Mutation {
# The input and the output are types generated by Ent.
createTodo(input: CreateTodoInput!): Todo
}

2. 将自定义 GraphQL 模式添加到 gqlgen.yml 配置:

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

3. 运行代码生成:

go generate .

如你所见,gqlgen 为我们生成具有 createTodo 解析器的名为 todo.resolvers.go 的新文件。 让我们将它关联到 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 如何自动支持它。