跳到主要内容

GraphQL 集成

Ent 框架使用 99designs/gqlgen 库支持 GraphQL,并提供了多种集成功能,例如:

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

更多信息请查阅网站的 GraphQL 教程

快速入门

要为项目启用 entgql 扩展,你需要按照此处所述使用 entc(Ent 代码生成)包。按照以下三个步骤为你的项目启用它:

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

ent/entc.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 文件:

ent/generate.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 目前包含几个示例:

  1. 一个完整的 GraphQL 服务器,包含一个使用数字 ID 字段的简单 Todo 应用
  2. 与示例 1 相同的 Todo 应用,但使用 UUID 类型作为 ID 字段
  3. 与示例 1 和 2 相同的 Todo 应用,但使用带前缀的 ULIDPULID 作为 ID 字段。此示例通过前缀 ID 加上实体类型(而非采用通用 ID 中的 ID 空间分区)来支持 Relay 节点 API。

请注意,本文档正在开发中。所有代码部分位于 ent/contrib/entgql,一个待办事项应用的示例可以在 ent/contrib/entgql/todo 中找到。