跳至主要內容

钩子函数


钩子函数

钩子函数也叫生命周期事件,在一些大型框架中都有这样的设计。是在执行 sequelize 中的调用之前或之后调用的函数,在项目开发中常常会遇到这样的情况:

希望在对某张表新增一条数据时,对某个字段自动操作

具体一点就是: 在表中新增一行数据,但是不希望通过提交SQL、代码的方式去维护一个字段,例如:创建时间。

这时候就可以使用钩子函数中的beforeUpdate函数,对该字段进行声明。

注意:钩子函数只能与模型一起使用,相当于模型的一个拓展,是对某种操作的一个抽象封装,无法与实例一起使用

提供的函数

sequelize提供了很多钩子函数,在源码存在一个hooks.js的文件,就是用来声明钩子函数hooks

钩子函数源码结构
钩子函数源码结构

例如:

//  插入数据成功前、后执行的函数
beforeCreate(attributes: M, options: CreateOptions<TAttributes>);
afterCreate(attributes: M, options: CreateOptions<TAttributes>);

// 更新数据成功前、后执行的函数
beforeUpdate(instance: M, options: InstanceUpdateOptions<TAttributes>);
afterUpdate(instance: M, options: InstanceUpdateOptions<TAttributes>);

// 批量更新成后前、后执行的函数
beforeBulkUpdate(options: UpdateOptions<TAttributes>);
afterBulkUpdate(options: UpdateOptions<TAttributes>);

// ...

更多钩子函数可以查看hooks源码

声明钩子

钩子函数的参数是通过引用传递的,因此可以在钩子函数中对模型操作的一些值进行修改,也就是对执行的一些SQL做修改,从而影响结果。

钩子函数也可以支持异步操作,这种情况下传入的钩子函数返回一个Promise对象即可。 例如:

// User为表实例,注意这里的async
User.beforeCreate(async (user, options) => {
  const hashedPassword = await hashPassword(user.password)
  user.password = hashedPassword
})

钩子函数的声明,目前支持三种形式:

  • 基于实例类继承sequelize提供的Model类,调用init()方法初始化
  • 使用addHook()方法,给实例类添加初始化操作
  • 直接使用绑在实例类上的钩子方法

具体的用法,可以参考下面的示例:

const { Model, Sequelize, DataTypes } = require('sequelize')

// 创建连接实例
const sequelize = new Sequelize('postgres://user:pass@142vip.cn:5432/142vip')

// 实例类继承sequelize提供的Model类
class User extends Model {
}

/**
 * 方法一:调用init()方法初始化
 */
User.init({
  username: DataTypes.STRING,
  mood: {
    type: DataTypes.ENUM,
    values: ['happy', 'sad'],
  },
}, {
  hooks: {
    beforeValidate: (user, _options) => {
      user.mood = 'happy'
    },
    afterValidate: (user, _options) => {
      user.username = 'Toni'
    },
  },
  sequelize,
})

// 方法二:使用addHook()方法
User.addHook('beforeValidate', (user, _options) => {
  user.mood = 'happy'
})

User.addHook('afterValidate', 'mood', (user, options) => {
  console.log(user, options)
  return Promise.reject(new Error('参数错误'))
})

/**
 * 方法三:直接使用绑在实例类上的钩子方法
 */
User.beforeCreate(async (user, _options) => {
  user.password = '123456'
})
User.afterValidate('userName', (user, _options) => {
  user.username = '142vip.cn'
})

移除钩子

在上面的钩子函数声明、使用中,可以发现模型实例类在继承Model父类之后,调用addHook() 函数,这是因为Model父类继承了Hooks类,支持对钩子函数做一些操作,查看源码:

继承Hooks类
继承Hooks类

同时,Hooks父类不仅提供addHook()方法,还提供一些其他的API:

Hooks类API
Hooks类API

支持对钩子函数的增加、移除等管理操作,例如:

const { Model, DataTypes, Sequelize: sequelize } = require('sequelize')

// BooK实例
class Book extends Model {
}

// Book实例类初始化
Book.init({
  title: DataTypes.STRING,
}, {
  sequelize,
})

/**
 * 为Book实例移除afterCreate类型的userName的钩子函数
 * - 按照名称
 */
Book.addHook('afterCreate', 'username', (_book, _options) => {
  // ...
})
Book.removeHook('afterCreate', 'username')

/**
 * 为Book实例移除afterCreate类型上所有的钩子函数
 */
Book.removeHook('afterCreate')

全局钩子

全局钩子是为所有模型运行的钩子,在插件中特别有用,可以定义想要的所有模型的行为。例如,自定义模型上的时间戳字段

const User = sequelize.define('User', {}, {
  tableName: 'tbl_user',
  hooks: {
    // 数据创建时,对创建时间、更新时间字段赋值
    beforeCreate: (record, options) => {
      record.dataValues.createdAt = new Date().getDate()
      record.dataValues.updatedAt = new Date().getDate()
    },
    // 数据更新时,只对更新时间字段赋值更新
    beforeUpdate: (record, options) => {
      record.dataValues.updatedAt = new Date().getDate()
    }
  }
})

绑在数据表实例上,每次对表有操作时就会执行。

默认钩子

在创建sequelize实例时利用define参数定义钩子函数,即可像所有模型添加对应的钩子函数,这个钩子函数在每个模型上都会执行,称为默认钩子

注意: 如果模型上对这个钩子函数有定义,则会覆盖实例上定义的钩子函数

这里通过对UserSpace表实例说明默认钩子函数,例如:

const { Sequelize } = require('sequelize')

const sequelize = new Sequelize({
  // ... 连接参数
  // 注意这里的define参数,用例指定默认钩子
  define: {
    hooks: {
      beforeCreate() {
      },
    },
  },
})
const User = sequelize.define('User', {})
const Space = sequelize.define('Space', {}, {
  hooks: {
    beforeCreate() {
    },
  },
})
// 运行默认钩子函数
await User.create({})
// 运行空间表实例自己的钩子函数,默认钩子被覆盖
await Space.create({})

Space实例的钩子函数覆盖掉默认钩子函数,所以只执行了实例上的钩子函数

永久钩子

在实例上定义默认钩子函数,可以在所有模型中执行,但前提是在模型定义上没有声明相同名称的钩子函数,否则就会被覆盖。

sequelize框架支持另外一种钩子函数,可以在所有模型中执行,也不用担心被覆盖的问题。提供sequelize.addHook()函数来 声明这种钩子函数,称为永久钩子,也可以理解为全局作用域下,不被覆盖的钩子函数。例如:

/**
 * 声明永久钩子,挂载到sequelize实例上
 */
sequelize.addHook('beforeCreate', () => {
  // 实现一些逻辑
})

不论表模型上是否声明、指定了自己的beforeCreate类型的钩子函数,这个永久钩子函数都会执行。执行顺序是:

  • 先执行表模型上的钩子函数
  • 再执行永久钩子函数

这里简单给出永久钩子的定义方式,例如:

/**
 * 声明永久钩子,挂载到sequelize实例上
 */
sequelize.addHook('beforeCreate', () => {
  // 实现一些逻辑
})

// 注意这里的sequelize对象上已经永久钩子函数
const User = sequelize.define('User', {})
const Project = sequelize.define('Project', {}, {
  hooks: {
    beforeCreate() {
      // ... 业务逻辑
    }
  }
})

// 运行全局钩子函数
await User.create({})
// 运行自己的钩子函数,然后再运行全局钩子函数
await Project.create({})

值的注意的是:还可以通过new sequelize的参数定义永久钩子函数,例如:

// 注意:这里没有利用define字段
new Sequelize(..., {
  hooks: {
    beforeCreate() {
    }
  }
});

使用define字段定义的是默认钩子函数,上面是定义全局钩子函数。查看源码有:

sequelize的构造函数
sequelize的构造函数

sequelize对象在创建时的options参数包含了对默认钩子函数永久钩子函数的定义。

连接钩子

Sequelize ORM框架除了提供对数据库表模型的的钩子函数外, 还提供了针对sequelize实例的钩子函数,查看源码:

sequelize的钩子函数
sequelize的钩子函数

支持在连接之前或者连接之后执行。当然和模型的钩子函数一样,支持异步执行,处理Promise对象,例如:

/**
 * 在sequelize建立连接前执行钩子函数,赋值连接密码
 */
sequelize.beforeConnect(async (config) => {
  config.password = 123456
})

使用连接池技术建立连接时,也是支持钩子函数的,配置、使用和上面类似。

注意: sequelize实例上的钩子函数只能声明为永久全局钩子函数, 因为建立连接后的实例对象是所有模型共享。