跳到主要内容

GraphQL 集成

Ent 框架使用 99designs/gqlgen 库以支持 GraphQL,并提供多种集成方式:

  1. 为 Ent 模式中定义的节点和边生成 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 在使用构建标签时会被忽略,而会由 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 中包含几个示例:

  1. 一个完整的 GraphQL 服务,及带有数字型 ID 字段的 代办事项应用
  2. 同样是第一条的 代办事项应用, 但是 ID 字段是 UUID(全球唯一ID)。
  3. 同样是第一条和第二条的 代办事项应用,但是 ID 字段具有 ULID 前缀或是 PULID 。这个示例通过带有实体类型前缀的 ID 而非在 全球唯一ID 中以空格分隔的 ID 以支持 Relay 节点 API。

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