GraphQL 集成
Ent 框架使用 99designs/gqlgen 库支持 GraphQL,并提供了多种集成功能,例如:
- 为 Ent 模式中定义的节点(nodes)和边(edges)生成 GraphQL 模式。
- 自动生成
Query和Mutation解析器,并与 Relay 框架无缝集成。 - 支持过滤、分页(包括嵌套分页),并遵循 Relay 游标连接规范。
- 高效的字段收集,无需数据加载器即可解决 N+1 问题。
- 事务性变更操作,确保在失败时的一致性。
更多信息请查阅网站的 GraphQL 教程。
快速入门
要为项目启用 entgql 扩展,你需要按照此处所述使用 entc(Ent 代码生成)包。按照以下三个步骤为你的项目启用它:
1. 创建一个名为 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()
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
if err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex)); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
2. 编辑 ent/generate.go 文件以执行 ent/entc.go 文件:
package ent
//go:generate go run -mod=mod entc.go
请注意,ent/entc.go 使用构建标签(build tag)被忽略,并通过 generate.go 文件由 go generate 命令执行。完整示例可在 ent/contrib 仓库中找到。
3. 为你的 Ent 项目运行代码生成:
go generate ./...
运行代码生成后,以下附加内容将被添加到你的项目中。
节点 API
创建了一个名为 ent/gql_node.go 的新文件,该文件实现了 Relay 节点接口。
要在 GraphQL 解析器中使用新生成的 ent.Noder 接口,请将 Node 方法添加到查询解析器,并查看配置部分以了解如何使用它。
如果在模式迁移中使用了通用 ID 选项,则节点类型(NodeType)可以从 id 值派生,并可按以下方式使用:
func (r *queryResolver) Node(ctx context.Context, id int) (ent.Noder, error) {
return r.client.Noder(ctx, id)
}
但是,如果对全局唯一标识符使用自定义格式,可以按以下方式控制节点类型:
func (r *queryResolver) Node(ctx context.Context, guid string) (ent.Noder, error) {
typ, id := parseGUID(guid)
return r.client.Noder(ctx, id, ent.WithFixedNodeType(typ))
}
GQL 配置
以下是一个待办事项应用的配置示例,该示例存在于 ent/contrib/entgql/todo 中。
schema:
- todo.graphql
resolver:
# 指示 gqlgen 在模式文件旁边生成解析器。
layout: follow-schema
dir: .
# gqlgen 将在生成的 ent 包中搜索模式中的任何类型名称。
# 如果匹配,则使用它们;否则将创建新的类型。
autobind:
- entgo.io/contrib/entgql/internal/todo/ent
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
# ent.Noder 是由节点模板生成的新接口。
- entgo.io/contrib/entgql/internal/todo/ent.Noder
分页
分页模板根据 Relay 游标连接规范 添加分页支持。有关 Relay 规范的更多信息,请参阅其网站。
连接排序
排序选项允许我们对连接返回的边(edges)应用排序。
使用说明
- 如果命名约定得以保留,生成的类型将被
autobind到 GraphQL 类型(参见下面的示例)。 - 排序字段通常应被索引,以避免全表数据库扫描。
- 分页查询可以按单个字段排序(不支持
order by ... then by ...的语义)。
示例
让我们回顾一下为现有 GraphQL 类型添加排序所需的步骤。代码示例基于一个待办事项应用,该应用可在 ent/contrib/entql/todo 中找到。
在 ent/schema 中定义排序字段
可以通过使用 entgql.Annotation 注释任何可比较的 Ent 字段来定义排序。注意,给定的 OrderField 名称必须与 GraphQL 模式中的枚举值匹配。
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now).
Immutable().
Annotations(
entgql.OrderField("CREATED_AT"),
),
field.Enum("status").
NamedValues(
"InProgress", "IN_PROGRESS",
"Completed", "COMPLETED",
).
Annotations(
entgql.OrderField("STATUS"),
),
field.Int("priority").
Default(0).
Annotations(
entgql.OrderField("PRIORITY"),
),
field.Text("text").
NotEmpty().
Annotations(
entgql.OrderField("TEXT"),
),
}
}
以上就是所需的全部模式更改,确保运行 go generate 以应用它们。
在 GraphQL 模式中定义排序类型
接下来,我们需要在 GraphQL 模式中定义排序类型:
enum OrderDirection {
ASC
DESC
}
enum TodoOrderField {
CREATED_AT
PRIORITY
STATUS
TEXT
}
input TodoOrder {
direction: OrderDirection!
field: TodoOrderField
}
注意,命名必须采用 <T>OrderField / <T>Order 的形式,以便 autobind 到生成的 Ent 类型。或者,可以使用 @goModel 指令进行手动类型绑定。
向分页查询添加 orderBy 参数
type Query {
todos(
after: Cursor
first: Int
before: Cursor
last: Int
orderBy: TodoOrder
): TodoConnection!
}
以上就是 GraphQL 模式所需的全部更改,让我们运行 gqlgen 代码生成。
更新底层解析器
转到 Todo 解析器,更新它以将 orderBy 参数传递给 .Paginate() 调用:
func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder) (*ent.TodoConnection, error) {
return r.client.Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
)
}
在 GraphQL 中使用
query {
todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
text
}
}
}
}
字段收集
收集模板使用急切加载(eager-loading)为 Ent 边(edges)提供自动的 GraphQL 字段收集支持。这意味着,如果查询请求节点及其边,entgql 会自动向根查询添加 With<E> 步骤,因此,客户端将对数据库执行恒定数量的查询——并且这种机制是递归工作的。
例如,给定以下 GraphQL 查询:
query {
users(first: 100) {
edges {
node {
photos {
link
}
posts {
content
comments {
content
}
}
}
}
}
}
客户端将执行 1 次查询以获取用户,1 次以获取照片,另外 2 次以获取帖子及其评论(总共 4 次)。此逻辑适用于根查询/解析器以及节点(node(s))API。
模式配置
要为特定边配置此选项,请使用 entgql.Annotation,如下所示:
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
Annotations(entgql.Bind()).
From("parent").
// Bind 表示 GraphQL 模式中的边名称
// 等同于 Ent 模式中使用的名称。
Annotations(entgql.Bind()).
Unique(),
edge.From("owner", User.Type).
Ref("tasks").
// 映射 GraphQL 模式中定义的边名称。
Annotations(entgql.MapsTo("taskOwner")),
}
}
使用和配置
GraphQL 扩展还会在 gql_edge.go 文件中为节点生成边解析器,如下所示:
func (t *Todo) Children(ctx context.Context) ([]*Todo, error) {
result, err := t.Edges.ChildrenOrErr()
if IsNotLoaded(err) {
result, err = t.QueryChildren().All(ctx)
}
return result, err
}
但是,如果需要手动显式编写这些解析器,可以在 GraphQL 模式中添加 forceResolver 选项:
type Todo implements Node {
id: ID!
children: [Todo]! @goField(forceResolver: true)
}
然后,你可以在你的类型解析器上实现它。
func (r *todoResolver) Children(ctx context.Context, obj *ent.Todo) ([]*ent.Todo, error) {
// 在这里执行一些操作。
return obj.Edges.ChildrenOrErr()
}
枚举实现
枚举模板为 Ent 生成的枚举实现了 MarshalGQL/UnmarshalGQL 方法。
事务性变更操作
entgql.Transactioner 处理程序在事务中执行每个 GraphQL 变更操作。注入到解析器的客户端是一个事务性 ent.Client。因此,使用 ent.Client 的代码无需更改。要使用它,请按照以下步骤操作:
1. 在 GraphQL 服务器初始化中,按如下方式使用 entgql.Transactioner 处理程序:
srv := handler.NewDefaultServer(todo.NewSchema(client))
srv.Use(entgql.Transactioner{TxOpener: client})
2. 然后,在 GraphQL 变更操作中,使用上下文中的客户端,如下所示:
func (mutationResolver) CreateTodo(ctx context.Context, todo TodoInput) (*ent.Todo, error) {
client := ent.FromContext(ctx)
return client.Todo.
Create().
SetStatus(todo.Status).
SetNillablePriority(todo.Priority). // 注意:实际示例中此方法可能为 SetPriority
SetText(todo.Text).
SetNillableParentID(todo.Parent).
Save(ctx)
}
示例
ent/contrib 目前包含几个示例:
- 一个完整的 GraphQL 服务器,包含一个使用数字 ID 字段的简单 Todo 应用
- 与示例 1 相同的 Todo 应用,但使用 UUID 类型作为 ID 字段
- 与示例 1 和 2 相同的 Todo 应用,但使用带前缀的 ULID 或
PULID作为 ID 字段。此示例通过前缀 ID 加上实体类型(而非采用通用 ID 中的 ID 空间分区)来支持 Relay 节点 API。
请注意,本文档正在开发中。所有代码部分位于 ent/contrib/entgql,一个待办事项应用的示例可以在 ent/contrib/entgql/todo 中找到。