钩子

钩子

本指南涵盖 Lucid 模型钩子。你将学会如何:

  • 使用装饰器在模型类上定义钩子
  • 理解每个钩子的触发顺序
  • 编写 save、create、update 和 delete 的持久化钩子
  • 编写 find、fetch 和 paginate 的查询钩子
  • 从应用代码中动态注册钩子
  • 在操作不应触发钩子时跳过它们

概述

钩子是模型上的静态方法,Lucid 在特定生命周期事件之前或之后调用它。钩子将模型拥有的行为保留在模型上,因此密码哈希、审计日志、缓存失效和软删除过滤等操作都保留在模型旁边,而不是分散在控制器和服务中。

app/models/user.ts
import hash from '@adonisjs/core/services/hash'
import { beforeSave } from '@adonisjs/lucid/orm'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}

beforeSave 钩子每次保存 User 实例时都会运行,无论该保存转换为 INSERT 还是 UPDATE。$dirty.password 检查确保哈希仅在密码实际更改时运行;否则钩子会在每次更新时对已存储的哈希重新哈希。

每个钩子都可以是异步的。从 before 钩子抛出异常会取消操作并将错误传播给调用者。钩子按注册顺序运行。

可用钩子

钩子接收触发时机
beforeSave模型实例在 INSERT 和 UPDATE 之前
afterSave模型实例在 INSERT 和 UPDATE 之后
beforeCreate模型实例仅在 INSERT 之前
afterCreate模型实例仅在 INSERT 之后
beforeUpdate模型实例仅在 UPDATE 之前
afterUpdate模型实例仅在 UPDATE 之后
beforeDelete模型实例在 DELETE 之前
afterDelete模型实例在 DELETE 之后
beforeFind查询构建器在 first、findOrFail、find、findBy 及相关查找器之前
afterFind模型实例在同一组查找器之后
beforeFetch查询构建器在通过 exec 或 await 获取多行之前
afterFetch模型实例数组在获取多行之后
beforePaginate[countQuery, query] 元组在 paginate 运行之前
afterPaginateModelPaginator 实例在 paginate 返回之后

编写钩子

通过使用 @adonisjs/lucid/orm 中对应的装饰器装饰模型类上的静态方法来注册钩子。该方法接收模型实例(对于持久化和 find 钩子)或查询构建器(对于 beforeFind、beforeFetch 和 beforePaginate)。

app/models/project.ts
import { afterSave } from '@adonisjs/lucid/orm'
import { ProjectsSchema } from '#database/schema'
export default class Project extends ProjectsSchema {
@afterSave()
static async syncToAlgolia(project: Project) {
const syncService = await app.container.make(AlgoliaSyncService)
await syncService.syncProject(project)
}
}

你可以在一个模型上注册多个同类型的钩子。它们按注册顺序运行,对于装饰器注册的钩子,遵循装饰器在类体中出现的顺序。

生命周期顺序

当保存运行时,特定的 beforeCreate/beforeUpdate 钩子和共享的 beforeSave 钩子都会触发。顺序始终是:特定的 before 钩子先执行,然后是 beforeSave,然后是数据库写入,然后是特定的 after 钩子,最后是 afterSave。

创建新行:

beforeCreate → beforeSave → INSERT → afterCreate → afterSave

更新现有行:

beforeUpdate → beforeSave → UPDATE → afterUpdate → afterSave

删除行:

beforeDelete → DELETE → afterDelete

如果 before 钩子抛出异常,数据库操作不会执行,after 钩子也不会运行。错误会传播给调用者,这让你可以将抛出视为验证失败。

持久化钩子

beforeSave

在 INSERT 和 UPDATE 之前都会触发。用于适用于两条代码路径的不变量,如密码哈希和值规范化。

import hash from '@adonisjs/core/services/hash'
import { beforeSave } from '@adonisjs/lucid/orm'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}

在修改列之前检查 user.$dirty,这样钩子仅在相关属性实际更改时运行。

afterSave

在 INSERT 和 UPDATE 之后都会触发。用于成功持久化后的副作用,如搜索索引更新、缓存预热或领域事件。

import { afterSave } from '@adonisjs/lucid/orm'
import { ProjectsSchema } from '#database/schema'
export default class Project extends ProjectsSchema {
@afterSave()
static async syncToAlgolia(project: Project) {
const syncService = await app.container.make(AlgoliaSyncService)
await syncService.syncProject(project)
}
}

beforeCreate

仅在 INSERT 之前触发。用于分配存在于模型上的服务端生成默认值(而非数据库中的列默认值)。

import { beforeCreate } from '@adonisjs/lucid/orm'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@beforeCreate()
static assignAvatar(user: User) {
user.avatarUrl = getRandomAvatar()
}
}

afterCreate

在 INSERT 成功后触发。用于仅在首次插入时有意义的副作用,如发送欢迎邮件或创建默认的关联记录集。

import { afterCreate } from '@adonisjs/lucid/orm'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@afterCreate()
static async sendWelcomeEmail(user: User) {
await mail.send((message) => {
message.to(user.email).subject('Welcome')
})
}
}

beforeUpdate

在 UPDATE 之前触发。用于强制执行仅适用于更新的不变量,如阻止更改不可变字段。

import { beforeUpdate } from '@adonisjs/lucid/orm'
import { ProjectsSchema } from '#database/schema'
export default class Project extends ProjectsSchema {
@beforeUpdate()
static lockTenantId(project: Project) {
if (project.$dirty.tenantId) {
throw new Error('tenantId cannot be changed after creation')
}
}
}

afterUpdate

在 UPDATE 成功后触发。用于现有行更改后的副作用。

beforeDelete

在 DELETE 之前触发。用于在行被删除之前必须执行的验证或清理,如拒绝删除仍有依赖项的记录。

import { beforeDelete } from '@adonisjs/lucid/orm'
import { ProjectsSchema } from '#database/schema'
export default class Project extends ProjectsSchema {
@beforeDelete()
static async guardActiveProjects(project: Project) {
if (project.status === 'active') {
throw new Error('Active projects cannot be deleted')
}
}
}

afterDelete

在行被删除后触发。常见用途包括移除缓存副本、通知下游系统和清理文件。

import { afterDelete } from '@adonisjs/lucid/orm'
import { PostsSchema } from '#database/schema'
export default class Post extends PostsSchema {
@afterDelete()
static async removeFromCache(post: Post) {
await cache.delete(`post-${post.id}`)
}
}

查询钩子

查询钩子将行为附加到读取路径。它们在通过模型查询构建器加载模型时触发,包括底层构建查询的静态查找器。

beforeFind

在任何解析为单行的查询之前触发。钩子接收查询构建器,因此你可以附加适用于每次查找的过滤器。

查找类查询包括 Model.find、Model.findBy、Model.first、Model.findOrFail、Model.firstOrFail,以及模型查询构建器上的任何 .first() / .firstOrFail() 调用。

import { beforeFind } from '@adonisjs/lucid/orm'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@beforeFind()
static filterDeleted(query: ModelQueryBuilderContract<typeof User>) {
query.whereNull('deleted_at')
}
}

afterFind

在查找类查询返回后触发。接收模型实例。用于用派生值装饰该行或记录访问事件。

import { afterFind } from '@adonisjs/lucid/orm'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@afterFind()
static async trackAccess(user: User) {
await accessLog.record(user.id)
}
}

beforeFetch

在任何解析为多行的查询之前触发(通过模型查询构建器上的 await 或 .exec() 执行)。接收查询构建器,与 beforeFind 相同。

import { beforeFetch } from '@adonisjs/lucid/orm'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@beforeFetch()
static filterDeleted(query: ModelQueryBuilderContract<typeof User>) {
query.whereNull('deleted_at')
}
}

大多数软删除实现在 beforeFind 和 beforeFetch 上注册相同的过滤器,以覆盖所有读取路径。

afterFetch

在多行查询返回后触发。接收模型实例数组。

import { afterFetch } from '@adonisjs/lucid/orm'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@afterFetch()
static warmCache(users: User[]) {
for (const user of users) {
cache.set(`user:${user.id}`, user, '1 hour')
}
}
}

分页钩子

paginate 按以下顺序同时触发 fetch 钩子和 paginate 钩子:

beforePaginate → beforeFetch →(count + data 查询)→ afterPaginate → afterFetch

beforePaginate

接收两个查询构建器的元组:count 查询(用于计算 total)和主查询(用于获取页面数据)。修改两者以保持计数和结果同步。

import { beforePaginate } from '@adonisjs/lucid/orm'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@beforePaginate()
static filterDeleted([countQuery, query]: [
ModelQueryBuilderContract<typeof User>,
ModelQueryBuilderContract<typeof User>,
]) {
countQuery.whereNull('deleted_at')
query.whereNull('deleted_at')
}
}

afterPaginate

接收 paginate 将解析为的 ModelPaginator 实例。用于检查或日志记录;就地修改分页器的情况不常见。

import { afterPaginate } from '@adonisjs/lucid/orm'
import type { ModelPaginatorContract } from '@adonisjs/lucid/types/model'
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
@afterPaginate()
static logUsage(paginator: ModelPaginatorContract<User>) {
logger.debug({ count: paginator.all().length, page: paginator.currentPage }, 'users paginated')
}
}

钩子与事务

当模型在事务中被保存或删除时,其 after 钩子在事务提交之前触发。如果事务随后回滚,该行实际上从未进入数据库,但钩子已经执行了。钩子执行的副作用(发送邮件、入队后台任务、调用 webhook、使缓存失效)将处于与实际数据库状态不一致的孤立状态。

要保证副作用仅在事务提交后运行,请通过 $trx 检查模型是否绑定到事务,如果是,则将工作附加到事务的 after('commit', ...) 钩子。在事务之外,直接运行副作用。

app/models/project.ts
import { afterSave } from '@adonisjs/lucid/orm'
import queue from '@adonisjs/queue/services/queue'
import { ProjectsSchema } from '#database/schema'
export default class Project extends ProjectsSchema {
@afterSave()
static async enqueueIndexJob(project: Project) {
const indexProject = () => queue.dispatch('projects.index', { id: project.id })
if (project.$trx) {
project.$trx.after('commit', indexProject)
} else {
await indexProject()
}
}
}

当保存在事务外运行时,project.$trx 为 undefined,任务立即派发。当保存在事务内运行时,派发被延迟到事务提交,如果事务回滚则永远不会触发。参见事务钩子了解 trx.after('commit', ...) 的完整行为,包括对注册回调应用的静默错误处理。

动态注册钩子

除了装饰器之外,你还可以通过 Model.before(event, handler) 和 Model.after(event, handler) 在运行时注册钩子。当钩子需要从服务提供者、插件或测试设置中注册,而非在模型上内联声明时,使用此模式。

providers/app_provider.ts
import User from '#models/user'
import hash from '@adonisjs/core/services/hash'
export default class AppProvider {
async boot() {
User.before('save', async (user) => {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
})
}
}

事件名称是不带 before/after 前缀的生命周期名称,例如 save、create、update、delete、find、fetch 或 paginate。动态注册和装饰器注册的钩子共享同一队列,并按添加顺序触发。

跳过钩子

有三种路径可以绕过钩子。当钩子执行的副作用不适合当前操作时使用它们:

  • *Quietly 变体(createQuietly、createManyQuietly、saveQuietly、deleteQuietly)运行相同的持久化操作但跳过该操作的所有钩子。在种子器、迁移和备份恢复中很有用。参见 CRUD 操作指南。
  • 查询构建器上的 pojo() 返回普通 JavaScript 对象而非模型实例。不会运行 afterFind 或 afterFetch 钩子,因为钩子操作的是模型实例。参见模型查询构建器指南。
  • 通过查询构建器的批量更新和删除(Model.query().update(...)、Model.query().delete())完全绕过实例级钩子,因为没有加载模型实例。参见使用查询构建器批量更新。