TL;DR
我们为 Ent GraphQL 扩展新增了一个集成,可从 ent/schema 生成类型安全的 GraphQL 过滤器(即 Where 预测子),并允许用户无缝地将 GraphQL 查询映射到 Ent 查询。
例如,获取所有 COMPLETED 任务项,可以执行以下请求:
query QueryAllCompletedTodos {
todos(
where: {
status: COMPLETED,
},
) {
edges {
node {
id
}
}
}
}
生成的 GraphQL 过滤器遵循 Ent 语法。这意味着以下查询也同样有效:
query FilterTodos {
todos(
where: {
or: [
{
hasParent: false,
status: COMPLETED,
},
{
status: IN_PROGRESS,
hasParentWith: {
priorityLT: 1,
statusNEQ: COMPLETED,
},
}
]
},
) {
edges {
node {
id
}
}
}
}
背景
许多处理 Go 中数据的库倾向于传递空接口实例 (interface{}),并在运行时使用反射来确定如何将数据映射到结构体字段。除了使用反射导致的性能损失之外,团队面临的最大负面影响是失去了类型安全。
当 API 是显式并在编译时(甚至在键入时)已知时,开发者所收到的大量错误反馈几乎是即时的。许多缺陷会被提早发现,开发过程也会更有趣!
Ent 被设计为为使用大型数据模型构建应用程序的团队提供出色的开发体验。为此,我们早期就决定 Ent 的核心设计原则之一是“使用代码生成的静态类型显式 API”。这意味着,对于 ent/schema 中定义的每个实体,都会生成显式、类型安全的代码,供开发者高效地交互。例如,在 ent 存储库的 Filesystem 示例 中,你会找到名为 File 的 schema:
// File holds the schema definition for the File entity.
type File struct {
ent.Schema
}
// Fields of the File.
func (File) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Bool("deleted").
Default(false),
field.Int("parent_id").
Optional(),
}
}
运行 Ent 代码生成后,将生成许多谓词函数。例如,以下函数可用于根据 name 字段过滤 File:
package file
// .. truncated ..
// Name applies the EQ predicate on the "name" field.
func Name(v string) predicate.File {
return predicate.File(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldName), v))
})
}
GraphQL 是由 Facebook 最初创建的 API 查询语言。与 Ent 类似,GraphQL 以图形概念建模数据,并支持类型安全查询。大约一年前,我们发布了 Ent 与 GraphQL 之间的集成。与 gRPC 集成 类似,该集成的目标是允许开发者轻松创建映射到 Ent 的 API 服务器,以变更和查询数据库中的数据。
自动 GraphQL 过滤器生成
在最近的一项社区调查中,Ent + GraphQL 集成被提及为 Ent 项目最受欢迎的功能之一。直到今天,该集成仅允许用户对数据执行有用但基础的查询。今天,我们宣布发布了一个功能,我们认为这将为 Ent 用户打开许多有趣的新用例:"自动 GraphQL 过滤器生成"。
正如我们之前所见,Ent 代码生成为我们维护了一套谓词函数,在我们的 Go 代码库中,它们可以轻松且显式地过滤数据库表中的数据。长期以来,这种功能(至少不是自动化的)并未为 Ent + GraphQL 集成的用户提供。通过自动 GraphQL 过滤器生成,开发者只需一次性配置,即可在他们的 GraphQL schema 中添加完整的“Filter Input Types”,可用作查询的谓词。此外,实施提供了运行时代码,用于解析这些谓词并将其映射到 Ent 查询。让我们看看它的实现:
生成 Filter Input Types
要为 ent/schema 包中的每种类型生成输入过滤器(例如 TodoWhereInput),请按如下方式编辑 ent/entc.go 配置文件:
// +build ignore
package main
import (
"log"
"entgo.io/contrib/entgql"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)
func main() {
ex, err := entgql.NewExtension(
entgql.WithWhereFilters(true),
entgql.WithConfigPath("../gqlgen.yml"),
entgql.WithSchemaPath("<PATH-TO-GRAPHQL-SCHEMA>"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
如果你是 Ent 和 GraphQL 新手,请参阅 快速开始教程。
接下来,运行 go generate ./ent/...。你会看到 Ent 已为每个 schema 类型生成了 <T>WhereInput。Ent 还会更新 GraphQL schema,因此你无需手动 autobind 它们到 gqlgen。例如:
// TodoWhereInput represents a where input for filtering Todo queries.
type TodoWhereInput struct {
Not *TodoWhereInput `json:"not,omitempty"`
Or []*TodoWhereInput `json:"or,omitempty"`
And []*TodoWhereInput `json:"and,omitempty"`
// "created_at" field predicates.
CreatedAt *time.Time `json:"createdAt,omitempty"`
CreatedAtNEQ *time.Time `json:"createdAtNEQ,omitempty"`
CreatedAtIn []time.Time `json:"createdAtIn,omitempty"`
CreatedAtNotIn []time.Time `json:"createdAtNotIn,omitempty"`
CreatedAtGT *time.Time `json:"createdAtGT,omitempty"`
CreatedAtGTE *time.Time `json:"createdAtGTE,omitempty"`
CreatedAtLT *time.Time `json:"createdAtLT,omitempty"`
CreatedAtLTE *time.Time `json:"createdAtLTE,omitempty"`
// "status" field predicates.
Status *todo.Status `json:"status,omitempty"`
StatusNEQ *todo.Status `json:"statusNEQ,omitempty"`
StatusIn []todo.Status `json:"statusIn,omitempty"`
StatusNotIn []todo.Status `json:"statusNotIn,omitempty"`
// .. truncated ..
}
"""
TodoWhereInput is used for filtering Todo objects.
Input was generated by ent.
"""
input TodoWhereInput {
not: TodoWhereInput
and: [TodoWhereInput!]
or: [TodoWhereInput!]
"""created_at field predicates"""
createdAt: Time
createdAtNEQ: Time
createdAtIn: [Time!]
createdAtNotIn: [Time!]
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
"""status field predicates"""
status: Status
statusNEQ: Status
statusIn: [Status!]
statusNotIn: [Status!]
# .. truncated ..
}
接下来,为完成集成,需要做两件事:
1. 编辑 GraphQL schema 以接受新的过滤类型:
type Query {
todos(
after: Cursor,
first: Int,
before: Cursor,
last: Int,
orderBy: TodoOrder,
where: TodoWhereInput,
): TodoConnection!
}
2. 在 GraphQL 解析器中使用新的过滤类型:
func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder, where *ent.TodoWhereInput) (*ent.TodoConnection, error) {
return r.client.Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
ent.WithTodoFilter(where.Filter),
)
}
过滤规范
正如前文提到的,借助新的 GraphQL 过滤类型,你可以表达与 Go 代码中使用的相同 Ent 过滤器。
并集、交集与否定
Not、And 和 Or 运算符可通过 not、and 和 or 字段添加。例如:
{
or: [
{
status: COMPLETED,
},
{
not: {
hasParent: true,
status: IN_PROGRESS,
}
}
]
}
当提供多个过滤字段时,Ent 会隐式添加 And 运算符。
{
status: COMPLETED,
textHasPrefix: "GraphQL",
}
上述查询将生成以下 Ent 查询:
client.Todo.
Query().
Where(
todo.And(
todo.StatusEQ(todo.StatusCompleted),
todo.TextHasPrefix("GraphQL"),
)
).
All(ctx)
边/关系过滤
边(关系)谓词可以用相同的 Ent 语法表达:
{
hasParent: true,
hasChildrenWith: {
status: IN_PROGRESS,
}
}
上述查询将生成以下 Ent 查询:
client.Todo.
Query().
Where(
todo.HasParent(),
todo.HasChildrenWith(
todo.StatusEQ(todo.StatusInProgress),
),
).
All(ctx)
实现示例
在 github.com/a8m/ent-graphql-example 中已有一个完整示例。
总结
正如我们之前讨论的,Ent 将“使用代码生成的静态类型显式 API”作为核心设计原则。通过自动 GraphQL 过滤器生成,我们进一步深化这一思想,提供与 RPC 层同等显式、类型安全的开发体验。
有问题吗?需要帮助入门?随时加入我们的 Discord 服务器 或 Slack 频道。
- 订阅我们的 Newsletter
- 在 Twitter 上关注我们
- 加入 #ent 在 Gophers Slack(Slack 频道)
- 加入我们的 Ent Discord 服务器