拦截器(Interceptors)
拦截器是用于处理各类 Ent 查询的执行中间件。 与钩子不同,拦截器应用于读取路径并以接口形式实现,使其能够在不同阶段拦截并修改查询,从而对查询行为实现更精细的控制。 示例请参见下文的 遍历器接口:
定义遍历器
用户可以声明实现了 Intercept 方法的结构体或使用预先定义的 ent.InterceptFunc 适配器来定义 Interceptor。
ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
// Do something before the query execution.
value, err := next.Query(ctx, query)
// Do something after the query execution.
return value, err
})
})
在上面的示例中,ent.Query 表示生成的查询构建器(例如 ent.<T>Query) 且访问其方法需要进行类型断言。例如:
ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
if q, ok := query.(*ent.UserQuery); ok {
q.Where(user.Name("a8m"))
}
return next.Query(ctx, query)
})
})
然而,intercept 功能标志生成的实用程序支持创建通用拦截器,这些拦截器可应用于任何查询类型。
intercept 功能标志可通过以下两种方式之一添加到项目中:
配置
- CLI
- Go
如果你在使用默认的 go generate 配置,添加 --feature intercept 选项到 ent/generate.go 文件中:
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept ./schema
推荐在使用 intercept 标志时添加 schema/snapshot 功能标志来增强开发体验,例如:
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept,schema/snapshot ./schema
如果你使用 GraphQL 文档中的配置,添加如下功能标志:
// +build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)
func main() {
opts := []entc.Option{
entc.FeatureNames("intercept"),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
推荐在使用 intercept 标志时添加 schema/snapshot 功能标志来增强开发体验,例如:
opts := []entc.Option{
- entc.FeatureNames("intercept"),
+ entc.FeatureNames("intercept", "schema/snapshot"),
}
拦截器注册
你应当注意与模式钩子 相同,
如果在模式中使用 Interceptors 选项,必须 在 main 包中添加下面的导入,
因为在模式包和生成的 ent 包之间可能会有循环导入:
import _ "<project>/ent/runtime"
使用生成的intercept包
一旦功能标志添加到项目中,可以使用 intercept 包创建拦截器:
- intercept.Func
- intercept.TraverseFunc
- intercept.NewQuery
client.Intercept(
intercept.Func(func(ctx context.Context, q intercept.Query) error {
// Limit all queries to 1000 records.
q.Limit(1000)
return nil
})
)
client.Intercept(
intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
// Apply a predicate/filter to all queries.
q.WhereP(predicate)
return nil
})
)
ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
// Get a generic query from a typed-query.
q, err := intercept.NewQuery(query)
if err != nil {
return nil, err
}
q.Limit(1000)
return next.Intercept(ctx, query)
})
})
定义遍历(Traverser)
在某些情况下,需要拦截 图遍历 操作,并在继续处理查询返回的节点之前修改其构建器。
例如,在下面的查询中,我们希望确保系统中的 任何 图遍历操作都仅遍历 active 用户:
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
q.Where(user.Active(true))
return nil
})
在定义和注册如下遍历器后,它在系统所有的图遍历中都起作用:
func TestTypedTraverser(t *testing.T) {
ctx := context.Background()
client := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&_fk=1")
defer client.Close()
a8m, nat := client.User.Create().SetName("a8m").SaveX(ctx), client.User.Create().SetName("nati").SetActive(false).SaveX(ctx)
client.Pet.CreateBulk(
client.Pet.Create().SetName("a").SetOwner(a8m),
client.Pet.Create().SetName("b").SetOwner(a8m),
client.Pet.Create().SetName("c").SetOwner(nat),
).ExecX(ctx)
// Get pets of all users.
if n := client.User.Query().QueryPets().CountX(ctx); n != 3 {
t.Errorf("got %d pets, want 3", n)
}
// Add an interceptor that filters out inactive users.
client.User.Intercept(
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
q.Where(user.Active(true))
return nil
}),
)
// Only pets of active users are returned.
if n := client.User.Query().QueryPets().CountX(ctx); n != 2 {
t.Errorf("got %d pets, want 2", n)
}
}
拦截器 vs. 遍历器
Interceptors 和 Traversers 都可以用来修改查询行为,但它们在执行过程中作用于不同阶段。
拦截器作为中间件,可在查询执行前进行修改,并在记录从数据库返回后进行修改。
因此它们仅应用于查询的最终阶段——即在数据库上实际执行语句时。
而遍历器则在更早阶段被调用,在图遍历的每个步骤中都能在他们合并到一起之前修改中间查询和最终查询。
总的来说,遍历功能更适用于图遍历时添加默认的过滤器,拦截功能更适用于在应用程序中实现日志和缓存能力。
client.User.Query().
QueryGroups(). // User traverse functions applied.
QueryPosts(). // Group traverse functions applied.
All(ctx) // Post traverse and intercept functions applied.
示例
软删除
软删除模式是拦截器和钩子常用的场景。下例演示了如何使用 ent.Mixin 为项目中的所有模式添加此类功能:
- Mixin
- Mixin usage
- Runtime usage
// SoftDeleteMixin implements the soft delete pattern for schemas.
type SoftDeleteMixin struct {
mixin.Schema
}
// Fields of the SoftDeleteMixin.
func (SoftDeleteMixin) Fields() []ent.Field {
return []ent.Field{
field.Time("delete_time").
Optional(),
}
}
type softDeleteKey struct{}
// SkipSoftDelete returns a new context that skips the soft-delete interceptor/mutators.
func SkipSoftDelete(parent context.Context) context.Context {
return context.WithValue(parent, softDeleteKey{}, true)
}
// Interceptors of the SoftDeleteMixin.
func (d SoftDeleteMixin) Interceptors() []ent.Interceptor {
return []ent.Interceptor{
intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
// Skip soft-delete, means include soft-deleted entities.
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
return nil
}
d.P(q)
return nil
}),
}
}
// Hooks of the SoftDeleteMixin.
func (d SoftDeleteMixin) Hooks() []ent.Hook {
return []ent.Hook{
hook.On(
func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Skip soft-delete, means delete the entity permanently.
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
return next.Mutate(ctx, m)
}
mx, ok := m.(interface {
SetOp(ent.Op)
Client() *gen.Client
SetDeleteTime(time.Time)
WhereP(...func(*sql.Selector))
})
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T", m)
}
d.P(mx)
mx.SetOp(ent.OpUpdate)
mx.SetDeleteTime(time.Now())
return mx.Client().Mutate(ctx, m)
})
},
ent.OpDeleteOne|ent.OpDelete,
),
}
}
// P adds a storage-level predicate to the queries and mutations.
func (d SoftDeleteMixin) P(w interface{ WhereP(...func(*sql.Selector)) }) {
w.WhereP(
sql.FieldIsNull(d.Fields()[0].Descriptor().Name),
)
}
// Pet holds the schema definition for the Pet entity.
type Pet struct {
ent.Schema
}
// Mixin of the Pet.
func (Pet) Mixin() []ent.Mixin {
return []ent.Mixin{
SoftDeleteMixin{},
}
}
// Filter out soft-deleted entities.
pets, err := client.Pet.Query().All(ctx)
if err != nil {
return err
}
// Include soft-deleted entities.
pets, err := client.Pet.Query().All(schema.SkipSoftDelete(ctx))
if err != nil {
return err
}
限制记录数
下面的示例演示了如何使用拦截器功能限制数据库返回的记录数量:
client.Intercept(
intercept.Func(func(ctx context.Context, q intercept.Query) error {
// LimitInterceptor limits the number of records returned from
// the database to 1000, in case Limit was not explicitly set.
if ent.QueryFromContext(ctx).Limit == nil {
q.Limit(1000)
}
return nil
}),
)
多项目支持
本示例演示如何写一个可以在多项目中使用的同样拦截器:
- Definition
- Usage
// Project-level example. The usage of "entgo" package emphasizes that this interceptor does not rely on any generated code.
func SharedLimiter[Q interface{ Limit(int) }](f func(entgo.Query) (Q, error), limit int) entgo.Interceptor {
return entgo.InterceptFunc(func(next entgo.Querier) entgo.Querier {
return entgo.QuerierFunc(func(ctx context.Context, query entgo.Query) (entgo.Value, error) {
l, err := f(query)
if err != nil {
return nil, err
}
l.Limit(limit)
// LimitInterceptor limits the number of records returned from the
// database to the configured one, in case Limit was not explicitly set.
if entgo.QueryFromContext(ctx).Limit == nil {
l.Limit(limit)
}
return next.Query(ctx, query)
})
})
}
client1.Intercept(SharedLimiter(intercept1.NewQuery, limit))
client2.Intercept(SharedLimiter(intercept2.NewQuery, limit))