GraphQL 集成
Ent 框架使用 99designs/gqlgen 库以支持 GraphQL,并提供多种集成方式:
- 为 Ent 模式中定义的节点和边生成 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 在使用构建标签时会被忽略,而会由 go generate 通过 generate.go 文件来执行。
完整示例参见 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:
# Tell gqlgen to generate resolvers next to the schema file.
layout: follow-schema
dir: .
# gqlgen will search for any type names in the schema in the generated
# ent package. If they match it will use them, otherwise it will new ones.
autobind:
- entgo.io/contrib/entgql/internal/todo/ent
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
# ent.Noder is the new interface generated by the Node template.
- entgo.io/contrib/entgql/internal/todo/ent.Noder
分页
分页模板根据 Relay 光标连接规范 添加了分页支持。 有关 Relay 规范的更多信息,请访问其 官方网站。
连接排序
排序选项允许我们为连接返回的边进行排序。
使用说明
- 如果遵循命名规范(参见下面例子)则生成的类型会被
自动绑定到 GraphQL 类型。 - 排序字段通常应建立 索引,以避免对整个数据库表进行全表扫描。
- 分页查询可按单一字段排序(不支持按...排序后再按...的语义)。
示例
让我们回顾一下为现有 GraphQL 类型添加排序功能所需的步骤。 代码示例基于一个待办事项应用程序,该应用程序位于 ent/contrib/entql/todo。
在 ent/schema 中定义排序字段
排序可通过为 ent 的任意可比较字段添加 entgql.Annotation 注解来定义。
请注意,指定的 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 形式以 自动绑定 生成的 ent 类型。
或使用 @goModel 指令进行手动类型绑定。
在分页查询中添加 orderBy 参数
type Query {
todos(
after: Cursor
first: Int
before: Cursor
last: Int
orderBy: TodoOrder
): TodoConnection!
}
这是全部所需的 GraphQL 模式变更,运行 gqlgen 进行代码生成。
更新底层解析器
前往 Todo 解析器并更新,在 .Paginate() 调用中传递 orderBy 参数:
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
}
}
}
}
字段集合
集合模板使用延迟加载增加了针对 ent 边的自动 GraphQL 字段集合 的支持。
这意味着如果查询请求节点及其边,entgql 会自动添加 With<E> 步骤到根查询中,作为结果,客户端对于数据库执行的查询保持恒定 —— 即递归查询。
例如,以下 GraphQL 查询:
query {
users(first: 100) {
edges {
node {
photos {
link
}
posts {
content
comments {
content
}
}
}
}
}
}
客户端会执行一次获取用户的查询、一次获取照片的查询及另外两个获取文章的查询和获取评论的查询(总共四个)。 这个逻辑对于根查询/解析器和节点 API 也适用。
模式配置
使用 entgql.Annotation 针对特定的边进行选项的配置:
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
Annotations(entgql.Bind()).
From("parent").
// Bind implies the edge name in graphql schema is
// equivalent to the name used in ent schema.
Annotations(entgql.Bind()).
Unique(),
edge.From("owner", User.Type).
Ref("tasks").
// Map edge names as defined in graphql schema.
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) {
// Do something here.
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).
SetText(todo.Text).
SetNillableParentID(todo.Parent).
Save(ctx)
}
示例
目前在 ent/contrib 中包含几个示例:
- 一个完整的 GraphQL 服务,及带有数字型 ID 字段的 代办事项应用
- 同样是第一条的 代办事项应用, 但是 ID 字段是 UUID(全球唯一ID)。
- 同样是第一条和第二条的 代办事项应用,但是 ID 字段具有 ULID 前缀或是
PULID。这个示例通过带有实体类型前缀的 ID 而非在 全球唯一ID 中以空格分隔的 ID 以支持 Relay 节点 API。
请注意本文档仍在开发中。所有代码位于 ent/contrib/entgql, 代办事项应用示例可以在 ent/contrib/entgql/todo 找到。