Sequelize 钩子系统详解 - 模型生命周期管理
发布时间:2024-03-26
作者:一介布衣
标签:Sequelize, 钩子, 生命周期, 事件处理
前言
今天咱们来学习 Sequelize 的钩子(Hooks)系统。说实话,钩子是 Sequelize 中一个非常强大但经常被忽视的功能。它能让你在模型的生命周期中的特定时刻执行自定义逻辑。
我记得刚开始用 Sequelize 的时候,总是在业务代码里写一堆重复的逻辑,比如密码加密、日志记录、缓存清理等等。后来发现了钩子系统,这些重复的工作都可以在模型层面自动处理,代码变得清爽了很多。
今天我就把钩子系统的各种用法和最佳实践分享给大家。
钩子基础概念
什么是钩子?
钩子是在模型操作的特定时刻自动执行的函数。Sequelize 提供了丰富的钩子类型,覆盖了模型的整个生命周期:
- 创建前后:beforeCreate、afterCreate
- 更新前后:beforeUpdate、afterUpdate
- 删除前后:beforeDestroy、afterDestroy
- 验证前后:beforeValidate、afterValidate
- 保存前后:beforeSave、afterSave
钩子的执行顺序
javascript
// 创建操作的钩子执行顺序
beforeValidate
afterValidate
beforeCreate
beforeSave
afterCreate
afterSave
// 更新操作的钩子执行顺序
beforeValidate
afterValidate
beforeUpdate
beforeSave
afterUpdate
afterSave
// 删除操作的钩子执行顺序
beforeDestroy
afterDestroy
基础钩子使用
1. 模型级钩子
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING,
password: DataTypes.STRING,
lastLoginAt: DataTypes.DATE
}, {
hooks: {
// 创建前加密密码
beforeCreate: async (user, options) => {
if (user.password) {
const bcrypt = require('bcryptjs');
user.password = await bcrypt.hash(user.password, 10);
}
},
// 更新前加密密码
beforeUpdate: async (user, options) => {
if (user.changed('password')) {
const bcrypt = require('bcryptjs');
user.password = await bcrypt.hash(user.password, 10);
}
},
// 创建后发送欢迎邮件
afterCreate: async (user, options) => {
console.log(`新用户注册: \${user.username}`);
// 这里可以发送欢迎邮件
await sendWelcomeEmail(user.email);
},
// 删除前清理相关数据
beforeDestroy: async (user, options) => {
// 删除用户的所有文章
await Post.destroy({ where: { userId: user.id } });
// 清理缓存
await clearUserCache(user.id);
}
}
});
2. 实例级钩子
javascript
// 为特定实例添加钩子
const user = await User.findByPk(1);
user.addHook('beforeUpdate', 'logUpdate', async (user, options) => {
console.log(`用户 \${user.username} 即将更新`);
});
// 移除钩子
user.removeHook('beforeUpdate', 'logUpdate');
3. 全局钩子
javascript
// 为所有模型添加全局钩子
sequelize.addHook('beforeCreate', (instance, options) => {
console.log(`创建 \${instance.constructor.name} 实例`);
});
sequelize.addHook('afterCreate', (instance, options) => {
console.log(`\${instance.constructor.name} 创建完成`);
});
常用钩子场景
1. 密码加密
javascript
const bcrypt = require('bcryptjs');
const User = sequelize.define('User', {
username: DataTypes.STRING,
password: DataTypes.STRING
}, {
hooks: {
beforeCreate: async (user) => {
if (user.password) {
user.password = await bcrypt.hash(user.password, 10);
}
},
beforeUpdate: async (user) => {
if (user.changed('password')) {
user.password = await bcrypt.hash(user.password, 10);
}
}
}
});
// 添加密码验证方法
User.prototype.validatePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
2. 自动生成 UUID
javascript
const { v4: uuidv4 } = require('uuid');
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
primaryKey: true
},
username: DataTypes.STRING
}, {
hooks: {
beforeCreate: (user) => {
if (!user.id) {
user.id = uuidv4();
}
}
}
});
3. 自动生成 Slug
javascript
const slugify = require('slugify');
const Post = sequelize.define('Post', {
title: DataTypes.STRING,
slug: DataTypes.STRING,
content: DataTypes.TEXT
}, {
hooks: {
beforeCreate: (post) => {
if (post.title && !post.slug) {
post.slug = slugify(post.title, { lower: true });
}
},
beforeUpdate: (post) => {
if (post.changed('title')) {
post.slug = slugify(post.title, { lower: true });
}
}
}
});
4. 审计日志
javascript
const AuditLog = sequelize.define('AuditLog', {
tableName: DataTypes.STRING,
recordId: DataTypes.INTEGER,
action: DataTypes.ENUM('CREATE', 'UPDATE', 'DELETE'),
oldValues: DataTypes.JSON,
newValues: DataTypes.JSON,
userId: DataTypes.INTEGER
});
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
afterCreate: async (user, options) => {
await AuditLog.create({
tableName: 'Users',
recordId: user.id,
action: 'CREATE',
newValues: user.toJSON(),
userId: options.userId || null
});
},
afterUpdate: async (user, options) => {
const changes = {};
const oldValues = {};
for (const field of user.changed()) {
changes[field] = user[field];
oldValues[field] = user._previousDataValues[field];
}
await AuditLog.create({
tableName: 'Users',
recordId: user.id,
action: 'UPDATE',
oldValues,
newValues: changes,
userId: options.userId || null
});
},
beforeDestroy: async (user, options) => {
await AuditLog.create({
tableName: 'Users',
recordId: user.id,
action: 'DELETE',
oldValues: user.toJSON(),
userId: options.userId || null
});
}
}
});
高级钩子技巧
1. 条件钩子
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING,
isVip: DataTypes.BOOLEAN
}, {
hooks: {
afterCreate: async (user, options) => {
// 只为 VIP 用户发送特殊欢迎邮件
if (user.isVip) {
await sendVipWelcomeEmail(user.email);
} else {
await sendRegularWelcomeEmail(user.email);
}
},
beforeUpdate: async (user, options) => {
// 如果用户升级为 VIP,发送通知
if (user.changed('isVip') && user.isVip) {
await sendVipUpgradeNotification(user.email);
}
}
}
});
2. 异步钩子处理
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
afterCreate: async (user, options) => {
// 并行执行多个异步操作
await Promise.all([
sendWelcomeEmail(user.email),
createUserProfile(user.id),
addToMailingList(user.email),
logUserRegistration(user.id)
]);
}
}
});
3. 钩子中的事务处理
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
afterCreate: async (user, options) => {
// 使用传入的事务
const transaction = options.transaction;
// 在同一事务中创建用户资料
await UserProfile.create({
userId: user.id,
displayName: user.username
}, { transaction });
// 创建默认设置
await UserSettings.create({
userId: user.id,
theme: 'light',
language: 'zh-CN'
}, { transaction });
}
}
});
4. 钩子中的错误处理
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
afterCreate: async (user, options) => {
try {
// 尝试发送欢迎邮件
await sendWelcomeEmail(user.email);
} catch (error) {
// 记录错误但不影响用户创建
console.error('发送欢迎邮件失败:', error);
// 可以选择将任务加入队列稍后重试
await addToEmailQueue({
type: 'welcome',
email: user.email,
userId: user.id
});
}
},
beforeDestroy: async (user, options) => {
try {
// 清理相关数据
await cleanupUserData(user.id);
} catch (error) {
// 如果清理失败,阻止删除操作
throw new Error(`清理用户数据失败: \${error.message}`);
}
}
}
});
批量操作钩子
1. 批量钩子
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING,
isActive: DataTypes.BOOLEAN
}, {
hooks: {
// 批量创建前
beforeBulkCreate: (instances, options) => {
console.log(`即将批量创建 \${instances.length} 个用户`);
// 为每个实例设置默认值
instances.forEach(instance => {
if (!instance.isActive) {
instance.isActive = true;
}
});
},
// 批量创建后
afterBulkCreate: (instances, options) => {
console.log(`成功创建 \${instances.length} 个用户`);
// 批量发送欢迎邮件
const emails = instances.map(user => user.email);
sendBulkWelcomeEmails(emails);
},
// 批量更新前
beforeBulkUpdate: (options) => {
console.log('即将执行批量更新:', options.where);
},
// 批量更新后
afterBulkUpdate: (options) => {
console.log('批量更新完成');
// 清理相关缓存
clearUsersCache();
},
// 批量删除前
beforeBulkDestroy: (options) => {
console.log('即将执行批量删除:', options.where);
},
// 批量删除后
afterBulkDestroy: (options) => {
console.log('批量删除完成');
}
}
});
2. 个体钩子 vs 批量钩子
javascript
// 使用 bulkCreate 时的钩子执行
await User.bulkCreate([
{ username: 'user1', email: 'user1@example.com' },
{ username: 'user2', email: 'user2@example.com' }
], {
individualHooks: true // 启用个体钩子
});
// individualHooks: false (默认)
// 只执行: beforeBulkCreate -> afterBulkCreate
// individualHooks: true
// 执行: beforeBulkCreate -> beforeValidate -> afterValidate -> beforeCreate -> afterCreate (对每个实例) -> afterBulkCreate
钩子性能优化
1. 避免在钩子中执行耗时操作
javascript
// 不好的做法:在钩子中执行耗时操作
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
afterCreate: async (user) => {
// 这会阻塞用户创建操作
await sendWelcomeEmail(user.email); // 可能很慢
await generateUserReport(user.id); // 可能很慢
}
}
});
// 好的做法:使用队列处理耗时操作
const Queue = require('bull');
const emailQueue = new Queue('email processing');
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
afterCreate: async (user) => {
// 快速将任务加入队列
await emailQueue.add('welcome', {
userId: user.id,
email: user.email
});
await emailQueue.add('report', {
userId: user.id
});
}
}
});
2. 批量操作优化
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
afterBulkCreate: async (instances, options) => {
// 批量处理而不是逐个处理
const userIds = instances.map(user => user.id);
const emails = instances.map(user => user.email);
// 批量创建用户资料
const profiles = userIds.map(userId => ({
userId,
displayName: `User \${userId}`
}));
await UserProfile.bulkCreate(profiles);
// 批量发送邮件
await sendBulkEmails(emails, 'welcome');
}
}
});
钩子调试和测试
1. 钩子调试
javascript
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
}, {
hooks: {
beforeCreate: (user, options) => {
console.log('beforeCreate 钩子执行');
console.log('用户数据:', user.toJSON());
console.log('选项:', options);
},
afterCreate: (user, options) => {
console.log('afterCreate 钩子执行');
console.log('创建的用户:', user.toJSON());
}
}
});
// 启用 Sequelize 日志查看钩子执行
const sequelize = new Sequelize(/* config */, {
logging: (msg) => {
if (msg.includes('HOOK')) {
console.log('钩子日志:', msg);
}
}
});
2. 钩子测试
javascript
// 测试钩子功能
describe('User 钩子', () => {
let mockSendEmail;
beforeEach(() => {
mockSendEmail = jest.fn();
// 模拟邮件发送函数
require('../utils/email').sendWelcomeEmail = mockSendEmail;
});
it('创建用户后应该发送欢迎邮件', async () => {
const user = await User.create({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
expect(mockSendEmail).toHaveBeenCalledWith('test@example.com');
expect(user.password).not.toBe('password123'); // 密码应该被加密
});
it('更新密码时应该重新加密', async () => {
const user = await User.create({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
const originalPassword = user.password;
await user.update({ password: 'newpassword' });
expect(user.password).not.toBe('newpassword');
expect(user.password).not.toBe(originalPassword);
});
});
实战案例:电商订单系统
javascript
// 订单模型的完整钩子系统
const Order = sequelize.define('Order', {
orderNumber: DataTypes.STRING,
userId: DataTypes.INTEGER,
totalAmount: DataTypes.DECIMAL(10, 2),
status: DataTypes.ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled'),
paymentMethod: DataTypes.STRING
}, {
hooks: {
// 创建前生成订单号
beforeCreate: async (order) => {
if (!order.orderNumber) {
order.orderNumber = await generateOrderNumber();
}
},
// 创建后的处理
afterCreate: async (order, options) => {
const transaction = options.transaction;
// 扣减库存
await reduceInventory(order.id, { transaction });
// 创建支付记录
await createPaymentRecord(order.id, { transaction });
// 发送订单确认邮件(异步)
setImmediate(() => {
sendOrderConfirmationEmail(order.userId, order.id);
});
},
// 更新前验证状态变更
beforeUpdate: async (order) => {
if (order.changed('status')) {
const oldStatus = order._previousDataValues.status;
const newStatus = order.status;
// 验证状态变更是否合法
if (!isValidStatusTransition(oldStatus, newStatus)) {
throw new Error(`不能从 \${oldStatus} 变更为 \${newStatus}`);
}
}
},
// 更新后的处理
afterUpdate: async (order) => {
if (order.changed('status')) {
const newStatus = order.status;
switch (newStatus) {
case 'paid':
await handleOrderPaid(order);
break;
case 'shipped':
await handleOrderShipped(order);
break;
case 'delivered':
await handleOrderDelivered(order);
break;
case 'cancelled':
await handleOrderCancelled(order);
break;
}
}
},
// 删除前清理
beforeDestroy: async (order, options) => {
const transaction = options.transaction;
// 恢复库存
await restoreInventory(order.id, { transaction });
// 处理退款
if (order.status === 'paid') {
await processRefund(order.id, { transaction });
}
}
}
});
// 辅助函数
async function generateOrderNumber() {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const random = Math.random().toString(36).substr(2, 6).toUpperCase();
return `ORD\${date}\${random}`;
}
function isValidStatusTransition(from, to) {
const validTransitions = {
'pending': ['paid', 'cancelled'],
'paid': ['shipped', 'cancelled'],
'shipped': ['delivered'],
'delivered': [],
'cancelled': []
};
return validTransitions[from]?.includes(to) || false;
}
async function handleOrderPaid(order) {
// 发送支付成功通知
await sendPaymentSuccessNotification(order.userId);
// 通知仓库发货
await notifyWarehouse(order.id);
}
async function handleOrderShipped(order) {
// 发送发货通知
await sendShippingNotification(order.userId, order.id);
// 更新物流信息
await updateShippingInfo(order.id);
}
总结
今天我们深入学习了 Sequelize 的钩子系统:
- ✅ 钩子的基本概念和执行顺序
- ✅ 模型级、实例级和全局钩子的使用
- ✅ 常用钩子场景的实现
- ✅ 高级钩子技巧和最佳实践
- ✅ 批量操作钩子的处理
- ✅ 性能优化和调试技巧
- ✅ 完整的电商订单系统案例
掌握了这些知识,你就能够:
- 在模型层面自动处理业务逻辑
- 保持代码的整洁和一致性
- 实现复杂的业务流程自动化
- 构建健壮的数据处理管道
钩子系统是 Sequelize 的一个强大特性,合理使用能让你的代码更加优雅和可维护!
相关文章推荐:
有问题欢迎留言讨论,我会及时回复大家!