过滤器输入
在本节中,我们将继续 GraphQL 示例,讲解如何从 ent/schema 生成类型安全的 GraphQL 过滤器(即 Where 谓词),并允许用户将 GraphQL 查询无缝映射到 Ent 查询。例如,以下 GraphQL 查询会映射到下方的 Ent 查询:
GraphQL
{
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 获取,每个步骤都附有 Git 标签。如果你想跳过基础设置,直接从 GraphQL 服务器的初始版本开始,可以克隆仓库并按如下方式运行程序:
git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
go run ./cmd/todo/
配置 Ent
打开你的 ent/entc.go 文件,并添加以下 4 行高亮显示的代码(扩展选项):
func main() {
ex, err := entgql.NewExtension(
entgql.WithSchemaGenerator(),
entgql.WithWhereInputs(true),
entgql.WithConfigPath("gqlgen.yml"),
entgql.WithSchemaPath("ent.graphql"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
entc.TemplateDir("./template"),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
WithWhereInputs 选项启用过滤器生成,WithConfigPath 配置 gqlgen 配置文件的路径,这允许扩展更准确地将 GraphQL 类型映射到 Ent 类型。最后一个选项 WithSchemaPath 则配置了用于将生成的过滤器写入的 GraphQL 架构的路径(可以是新的或现有的)。
更改 entc.go 配置后,我们准备好按如下方式执行代码生成:
go generate .
注意,Ent 已在名为 ent/gql_where_input.go 的文件中为你的架构中的每个类型生成了 <T>WhereInput。Ent 还生成了 GraphQL 架构 (ent.graphql),因此你无需手动将它们 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"`
// .. 截断 ..
}
"""
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!]
# .. 截断 ..
}
如果你的项目包含超过 1 个 GraphQL 架构(例如 todo.graphql 和 ent.graphql),你应该如下配置 gqlgen.yml 文件:
schema:
- todo.graphql
# ent.graphql 架构由 Ent 生成。
- ent.graphql
配置 GQL
运行代码生成后,我们准备好完成集成并在 GraphQL 中开放过滤功能:
1. 编辑 GraphQL 架构以接受新的过滤器类型:
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 运算符添加到 where 子句中。例如:
query {
todos(
where: {
or: [
{
status: COMPLETED
},
{
not: {
hasParent: true,
status: IN_PROGRESS
}
}
]
}
) {
edges {
node {
id
text
}
cursor
}
}
}
当提供多个过滤器字段时,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)
自定义过滤器
有时我们需要向过滤器添加自定义条件,虽然始终可以使用 模板 和 SchemaHooks,但这并不总是最简单的解决方案,特别是如果我们只想添加简单的条件。
幸运的是,通过结合使用 GraphQL 对象类型扩展 和自定义解析器,我们可以实现此功能。
让我们看一个添加自定义 isCompleted 过滤器的例子,该过滤器将接收一个布尔值并过滤所有具有 completed 状态的待办事项。
让我们从扩展 TodoWhereInput 开始:
extend input TodoWhereInput {
isCompleted: Boolean
}
运行代码生成后,我们应该在 todo.resolvers.go 文件中看到一个新的字段解析器:
func (r *todoWhereInputResolver) IsCompleted(ctx context.Context, obj *ent.TodoWhereInput, data *bool) error {
panic(fmt.Errorf("not implemented"))
}
我们现在可以使用 ent.TodoWhereInput 结构中的 AddPredicates 方法来实现自定义过滤:
func (r *todoWhereInputResolver) IsCompleted(ctx context.Context, obj *ent.TodoWhereInput, data *bool) error {
if obj == nil || data == nil {
return nil
}
if *data {
obj.AddPredicates(todo.StatusEQ(todo.StatusCompleted))
} else {
obj.AddPredicates(todo.StatusNEQ(todo.StatusCompleted))
}
return nil
}
我们可以像使用任何其他谓词一样使用这个新的过滤功能:
{
isCompleted: true,
}
# 包括 not、and 和 or 字段
{
not: {
isCompleted: true,
}
}
作为谓词使用
Filter 选项允许我们在任何类型的查询上将生成的 WhereInput 用作常规谓词:
query := ent.Todo.Query()
query, err := input.Filter(query)
if err != nil {
return nil, err
}
return query.All(ctx)
做得很好!如你所见,通过更改几行代码,我们的应用程序现在开放了类型安全的 GraphQL 过滤器,这些过滤器会自动映射到 Ent 查询。有问题吗?需要入门帮助?欢迎加入我们的 Discord 服务器 或 Slack 频道。