监听者与订阅者
原创2026/3/18大约 8 分钟
监听者(Listeners)和订阅者(Subscribers)是 TypeORM 实现“实体生命周期事件驱动”的核心能力,监听者聚焦单个实体的专属逻辑,订阅者实现跨实体的通用逻辑,可用于数据校验、自动填充、日志审计、业务触发等场景,替代传统的“硬编码”业务逻辑,提升代码复用性和可维护性。
一、监听者(Listeners)
监听者是实体级别的生命周期钩子,直接绑定到单个实体类中,通过装饰器标记,在实体的增删改查等操作前后执行自定义逻辑,是实现单个实体专属业务逻辑的首选方式。
1. 核心生命周期钩子
TypeORM 提供了覆盖实体全生命周期的钩子装饰器,每个钩子对应实体操作的关键节点:
| 钩子装饰器 | 触发时机 | 核心适用场景 |
|---|---|---|
| @BeforeInsert | 实体插入数据库前 | 密码加密、创建时间自动填充、数据校验 |
| @AfterInsert | 实体插入数据库后 | 插入日志记录、发送创建通知 |
| @BeforeUpdate | 实体更新数据库前 | 更新时间自动填充、修改权限校验 |
| @AfterUpdate | 实体更新数据库后 | 更新日志记录、缓存刷新 |
| @BeforeRemove | 实体物理删除前 | 删除权限校验、关联数据备份 |
| @AfterRemove | 实体物理删除后 | 删除日志记录、关联数据清理 |
| @BeforeSoftRemove | 实体软删除前 | 软删除权限校验 |
| @AfterSoftRemove | 实体软删除后 | 软删除日志记录 |
| @BeforeRecover | 软删除实体恢复前 | 恢复权限校验 |
| @AfterRecover | 软删除实体恢复后 | 恢复日志记录 |
2. 监听者使用示例(用户实体)
以用户密码加密、时间自动填充为例,展示监听者的核心用法:
import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate, AfterInsert, BeforeSoftRemove } from "typeorm";
import { hash, compare } from "bcryptjs"; // 密码加密/验证库(需安装:npm i bcryptjs)
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true, comment: "用户名" })
username: string;
@Column({ comment: "密码(加密存储)" })
password: string;
@Column({ type: "datetime", comment: "创建时间" })
createTime: Date;
@Column({ type: "datetime", comment: "更新时间" })
updateTime: Date;
// 临时存储原始密码(用于更新时判断是否修改)
originalPassword?: string;
// ===== 插入前逻辑 =====
@BeforeInsert()
async handleBeforeInsert() {
// 1. 密码加密(加盐值10,平衡安全性和性能)
this.password = await hash(this.password, 10);
// 2. 自动填充创建/更新时间
this.createTime = new Date();
this.updateTime = new Date();
// 3. 数据校验
if (!this.username || this.username.length < 3) {
throw new Error("用户名长度不能小于3位");
}
}
// ===== 更新前逻辑 =====
@BeforeUpdate()
async handleBeforeUpdate() {
// 仅当密码修改时重新加密(需先保存原始密码)
if (this.originalPassword && this.password !== this.originalPassword) {
this.password = await hash(this.password, 10);
}
// 自动更新时间
this.updateTime = new Date();
}
// ===== 插入后逻辑 =====
@AfterInsert()
handleAfterInsert() {
console.log(`[用户创建] 用户名:${this.username},ID:${this.id}`);
// 实际场景:可发送注册成功邮件、写入审计日志等
}
// ===== 软删除前校验 =====
@BeforeSoftRemove()
handleBeforeSoftRemove() {
// 禁止删除管理员账号
if (this.username === "admin") {
throw new Error("管理员账号不允许删除");
}
}
// 辅助方法:验证密码
async verifyPassword(plainPassword: string): Promise<boolean> {
return await compare(plainPassword, this.password);
}
}3. 监听者关键注意事项
- 钩子中的
this:指向当前实体实例,修改this的属性会同步到数据库(仅Before*钩子有效,After*钩子修改无意义); - 异步支持:钩子函数支持
async/await,TypeORM 会等待异步逻辑完成后再执行数据库操作; - 原始值获取:监听者中无法直接获取实体的数据库原始值(需手动保存,如示例中的
originalPassword); - 异常处理:钩子中抛出的异常会终止数据库操作,适合做数据校验。
二、订阅者(Subscribers)
订阅者是全局的事件监听机制,独立于实体类,可监听单个/多个/所有实体的生命周期事件,实现跨实体的通用逻辑(如全局审计日志、统一时间填充),复用性远高于监听者。
1. 订阅者核心配置
(1)创建订阅者类
订阅者需实现 EntitySubscriberInterface 接口,或继承 EventSubscriber 抽象类(推荐),通过 @EventSubscriber() 标记:
// src/subscriber/AuditLogSubscriber.ts(审计日志订阅者)
import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent, SoftRemoveEvent, RecoverEvent } from "typeorm";
import { User } from "../entity/User";
import { Order } from "../entity/Order";
// 标记为订阅者
@EventSubscriber()
export class AuditLogSubscriber implements EntitySubscriberInterface {
/**
* 指定监听的实体(可选)
* - 返回数组:监听指定实体
* - 不实现:监听所有实体
*/
listenTo() {
return [User, Order]; // 仅监听 User 和 Order 实体
}
// ===== 插入事件(前)=====
beforeInsert(event: InsertEvent<any>) {
console.log(`[审计日志-插入前] 实体:${event.entity.constructor.name},数据:`, event.entity);
// 通用逻辑:自动填充创建时间(所有实体通用)
if ("createTime" in event.entity && !event.entity.createTime) {
event.entity.createTime = new Date();
}
}
// ===== 插入事件(后)=====
afterInsert(event: InsertEvent<any>) {
this.saveAuditLog(event, "INSERT");
}
// ===== 更新事件(后)=====
afterUpdate(event: UpdateEvent<any>) {
// event.databaseEntity:数据库中的原始值;event.entity:修改后的值
console.log(`[审计日志-更新] 原始数据:`, event.databaseEntity);
this.saveAuditLog(event, "UPDATE");
}
// ===== 软删除事件(后)=====
afterSoftRemove(event: SoftRemoveEvent<any>) {
this.saveAuditLog(event, "SOFT_DELETE");
}
// ===== 恢复事件(后)=====
afterRecover(event: RecoverEvent<any>) {
this.saveAuditLog(event, "RECOVER");
}
// ===== 自定义:保存审计日志 =====
private async saveAuditLog(event: any, operation: string) {
const auditLog = {
entityName: event.entity.constructor.name, // 实体名
entityId: event.entity.id, // 实体ID
operation: operation, // 操作类型(INSERT/UPDATE/DELETE等)
operateTime: new Date(), // 操作时间
operator: "system", // 操作人(实际场景从上下文获取,如登录用户)
// 可选:记录修改前后的差异
oldData: event.databaseEntity ? JSON.stringify(event.databaseEntity) : null,
newData: JSON.stringify(event.entity),
};
// 打印日志(实际场景可写入数据库的 audit_log 表)
console.log("[审计日志]", auditLog);
// 示例:写入审计日志表
// await event.manager.save("audit_log", auditLog);
}
}(2)注册订阅者
需在 data-source.ts 中配置订阅者路径,让 TypeORM 加载并生效:
// data-source.ts
import { DataSource } from "typeorm";
import { AuditLogSubscriber } from "./subscriber/AuditLogSubscriber";
export const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "your-password",
database: "typeorm_demo",
entities: ["src/entity/**/*.ts"],
migrations: ["src/migration/**/*.ts"],
// 方式1:通配符加载所有订阅者(推荐)
subscribers: ["src/subscriber/**/*.ts"],
// 方式2:手动注册订阅者实例
// subscribers: [AuditLogSubscriber],
synchronize: false,
logging: true,
});2. 订阅者核心事件与参数
订阅者的事件方法与监听者钩子一一对应,事件参数包含丰富的上下文信息,是订阅者的核心优势:
| 事件方法 | 对应监听者钩子 | 事件参数类型 | 核心参数说明 |
|---|---|---|---|
| beforeInsert | @BeforeInsert | InsertEvent | entity:当前实体;manager:实体管理器;queryRunner:查询执行器(事务场景) |
| afterInsert | @AfterInsert | InsertEvent | 同上 |
| beforeUpdate | @BeforeUpdate | UpdateEvent | databaseEntity:数据库原始值;entity:修改后的值 |
| afterUpdate | @AfterUpdate | UpdateEvent | 同上 |
| beforeRemove | @BeforeRemove | RemoveEvent | 同上 |
| afterRemove | @AfterRemove | RemoveEvent | 同上 |
| beforeSoftRemove | @BeforeSoftRemove | SoftRemoveEvent | 同上 |
| afterSoftRemove | @AfterSoftRemove | SoftRemoveEvent | 同上 |
3. 订阅者高级用法
(1)监听所有实体
不实现 listenTo() 方法,订阅者会监听项目中所有实体的事件:
@EventSubscriber()
export class GlobalSubscriber implements EntitySubscriberInterface {
// 不实现 listenTo(),监听所有实体
afterInsert(event: InsertEvent<any>) {
console.log(`[全局日志] 新增实体:${event.entity.constructor.name},ID:${event.entity.id}`);
}
}(2)事务中执行订阅者逻辑
通过 queryRunner 确保订阅者逻辑在事务内执行,保证数据一致性:
afterInsert(event: InsertEvent<User>) {
// 判断是否在事务中
if (event.queryRunner && event.queryRunner.isTransactionActive) {
// 使用事务内的 queryRunner 执行操作,避免事务隔离问题
await event.queryRunner.manager.save("audit_log", {
entityName: "User",
entityId: event.entity.id,
operation: "INSERT",
})
} else {
// 非事务场景,使用全局 manager
await event.manager.save("audit_log", {
entityName: "User",
entityId: event.entity.id,
operation: "INSERT",
})
}
}(3)条件触发订阅者逻辑
根据实体属性或操作类型,选择性执行逻辑:
afterUpdate(event: UpdateEvent<Order>) {
// 仅当订单状态从 "unpaid" 改为 "paid" 时触发支付逻辑
if (
event.databaseEntity.status === "unpaid" &&
event.entity.status === "paid"
) {
console.log(`[订单支付] 订单号:${event.entity.orderNo},触发支付后逻辑`);
// 实际场景:通知仓库发货、生成物流单、扣减库存等
}
}三、监听者 vs 订阅者(核心差异)
| 特性 | 监听者(Listeners) | 订阅者(Subscribers) |
|---|---|---|
| 作用域 | 单个实体(仅当前实体生效) | 多个/所有实体(全局生效) |
| 复用性 | 低(仅适用于当前实体) | 高(通用逻辑可复用) |
| 配置方式 | 实体内部装饰器标记 | 独立类 + 数据源配置注册 |
| 上下文信息 | 仅实体实例(this) | 丰富的事件参数(entity、manager、原始值等) |
| 适用场景 | 单个实体的专属逻辑(如用户密码加密) | 全局通用逻辑(如审计日志、统一时间填充) |
四、最佳实践与问题排查
1. 最佳实践
(1)职责划分
- 实体专属逻辑用监听者:如用户密码加密、订单状态校验;
- 全局通用逻辑用订阅者:如审计日志、所有实体的时间自动填充。
(2)性能优化
- 避免在钩子/订阅者中执行耗时操作(如远程API调用),可通过消息队列异步处理;
- 异步逻辑必须使用
async/await,否则 TypeORM 会跳过异步操作; - 高并发场景,订阅者逻辑尽量轻量化,减少数据库操作。
(3)调试技巧
- 在钩子/订阅者中打印日志,排查执行顺序和数据问题;
- 使用
event.queryRunner.getSql()打印执行的SQL,确认数据修改是否生效; - 事务场景中,必须使用
event.queryRunner.manager执行操作,避免事务隔离问题。
2. 常见问题排查
(1)订阅者未触发
- 检查
data-source.ts中是否配置了订阅者路径; - 确认订阅者类添加了
@EventSubscriber()装饰器; - 若指定了
listenTo(),检查返回的实体是否正确。
(2)钩子中修改的属性未生效
- 仅
Before*钩子修改属性会同步到数据库,After*钩子修改无意义; - 异步钩子未添加
async/await,导致逻辑未执行完成。
(3)无法获取实体原始值
- 监听者:需手动保存原始值(如示例中的
originalPassword); - 订阅者:通过
event.databaseEntity获取数据库原始值(仅 Update/Remove 事件有效)。
至此,本章节的学习就到此结束了,如有疑惑,可对接技术客服进行相关咨询。