钩子

钩子

本指南涵盖 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模型实例INSERTUPDATE 之前
afterSave模型实例INSERTUPDATE 之后
beforeCreate模型实例仅在 INSERT 之前
afterCreate模型实例仅在 INSERT 之后
beforeUpdate模型实例仅在 UPDATE 之前
afterUpdate模型实例仅在 UPDATE 之后
beforeDelete模型实例DELETE 之前
afterDelete模型实例DELETE 之后
beforeFind查询构建器firstfindOrFailfindfindBy 及相关查找器之前
afterFind模型实例在同一组查找器之后
beforeFetch查询构建器在通过 execawait 获取多行之前
afterFetch模型实例数组在获取多行之后
beforePaginate[countQuery, query] 元组paginate 运行之前
afterPaginateModelPaginator 实例paginate 返回之后

编写钩子

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

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

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

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

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

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.findModel.findByModel.firstModel.findOrFailModel.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')
}
}

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

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.$trxundefined,任务立即派发。当保存在事务内运行时,派发被延迟到事务提交,如果事务回滚则永远不会触发。参见事务钩子了解 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 前缀的生命周期名称,例如 savecreateupdatedeletefindfetchpaginate。动态注册和装饰器注册的钩子共享同一队列,并按添加顺序触发。

跳过钩子

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

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