Skip to content

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 的一个强大特性,合理使用能让你的代码更加优雅和可维护!


相关文章推荐:

有问题欢迎留言讨论,我会及时回复大家!