钩子
本指南涵盖 Lucid 模型钩子。你将学会如何:
- 使用装饰器在模型类上定义钩子
- 理解每个钩子的触发顺序
- 编写 save、create、update 和 delete 的持久化钩子
- 编写 find、fetch 和 paginate 的查询钩子
- 从应用代码中动态注册钩子
- 在操作不应触发钩子时跳过它们
概述
钩子是模型上的静态方法,Lucid 在特定生命周期事件之前或之后调用它。钩子将模型拥有的行为保留在模型上,因此密码哈希、审计日志、缓存失效和软删除过滤等操作都保留在模型旁边,而不是分散在控制器和服务中。
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 运行之前 |
afterPaginate | ModelPaginator 实例 | 在 paginate 返回之后 |
编写钩子
通过使用 @adonisjs/lucid/orm 中对应的装饰器装饰模型类上的静态方法来注册钩子。该方法接收模型实例(对于持久化和 find 钩子)或查询构建器(对于 beforeFind、beforeFetch 和 beforePaginate)。
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', ...) 钩子。在事务之外,直接运行副作用。
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) 在运行时注册钩子。当钩子需要从服务提供者、插件或测试设置中注册,而非在模型上内联声明时,使用此模式。
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())完全绕过实例级钩子,因为没有加载模型实例。参见使用查询构建器批量更新。