钩子
钩子(也称为生命周期事件)是在执行 Sequelize 调用之前和之后调用的函数。例如,如果您希望在保存模型之前始终设置某个值,则可以添加一个 beforeUpdate
钩子。
注意:您不能将钩子与实例一起使用。钩子用于模型。
可用钩子
Sequelize 提供了许多钩子。完整列表可以在 源代码 - src/hooks.js 中直接找到。
钩子触发顺序
下图显示了最常用钩子的触发顺序。
注意:此列表并不详尽。
(1)
beforeBulkCreate(instances, options)
beforeBulkDestroy(options)
beforeBulkUpdate(options)
(2)
beforeValidate(instance, options)
[... validation happens ...]
(3)
afterValidate(instance, options)
validationFailed(instance, options, error)
(4)
beforeCreate(instance, options)
beforeDestroy(instance, options)
beforeUpdate(instance, options)
beforeSave(instance, options)
beforeUpsert(values, options)
[... creation/update/destruction happens ...]
(5)
afterCreate(instance, options)
afterDestroy(instance, options)
afterUpdate(instance, options)
afterSave(instance, options)
afterUpsert(created, options)
(6)
afterBulkCreate(instances, options)
afterBulkDestroy(options)
afterBulkUpdate(options)
声明钩子
钩子的参数是通过引用传递的。这意味着您可以更改值,并且这将反映在插入/更新语句中。钩子可能包含异步操作 - 在这种情况下,钩子函数应该返回一个 Promise。
目前有三种方法可以以编程方式添加钩子
// Method 1 via the .init() method
class User extends Model {}
User.init(
{
username: DataTypes.STRING,
mood: {
type: DataTypes.ENUM,
values: ['happy', 'sad', 'neutral'],
},
},
{
hooks: {
beforeValidate: (user, options) => {
user.mood = 'happy';
},
afterValidate: (user, options) => {
user.username = 'Toni';
},
},
sequelize,
},
);
// Method 2 via the .addHook() method
User.addHook('beforeValidate', (user, options) => {
user.mood = 'happy';
});
User.addHook('afterValidate', 'someCustomName', (user, options) => {
return Promise.reject(new Error("I'm afraid I can't let you do that!"));
});
// Method 3 via the direct method
User.beforeCreate(async (user, options) => {
const hashedPassword = await hashPassword(user.password);
user.password = hashedPassword;
});
User.afterValidate('myHookAfter', (user, options) => {
user.username = 'Toni';
});
删除钩子
只能删除具有 name 参数的钩子。
class Book extends Model {}
Book.init(
{
title: DataTypes.STRING,
},
{ sequelize },
);
Book.addHook('afterCreate', 'notifyUsers', (book, options) => {
// ...
});
Book.removeHook('afterCreate', 'notifyUsers');
您可以拥有许多名称相同的钩子。调用 .removeHook()
将删除所有这些钩子。
全局/通用钩子
全局钩子是在所有模型上运行的钩子。它们对于插件特别有用,并且可以定义您希望所有模型都具有的行为,例如允许使用模型上的 sequelize.define
自定义时间戳
const User = sequelize.define(
'User',
{},
{
tableName: 'users',
hooks: {
beforeCreate: (record, options) => {
record.dataValues.createdAt = new Date()
.toISOString()
.replace(/T/, ' ')
.replace(/\..+/g, '');
record.dataValues.updatedAt = new Date()
.toISOString()
.replace(/T/, ' ')
.replace(/\..+/g, '');
},
beforeUpdate: (record, options) => {
record.dataValues.updatedAt = new Date()
.toISOString()
.replace(/T/, ' ')
.replace(/\..+/g, '');
},
},
},
);
它们可以通过多种方式定义,这些方式的语义略有不同
默认钩子(在 Sequelize 构造函数选项上)
const sequelize = new Sequelize(..., {
define: {
hooks: {
beforeCreate() {
// Do stuff
}
}
}
});
这会向所有模型添加一个默认钩子,如果模型没有定义自己的 beforeCreate
钩子,则会运行该钩子
const User = sequelize.define('User', {});
const Project = sequelize.define(
'Project',
{},
{
hooks: {
beforeCreate() {
// Do other stuff
},
},
},
);
await User.create({}); // Runs the global hook
await Project.create({}); // Runs its own hook (because the global hook is overwritten)
永久钩子(使用 sequelize.addHook
)
sequelize.addHook('beforeCreate', () => {
// Do stuff
});
此钩子始终运行,无论模型是否指定自己的 beforeCreate
钩子。本地钩子始终在全局钩子之前运行
const User = sequelize.define('User', {});
const Project = sequelize.define(
'Project',
{},
{
hooks: {
beforeCreate() {
// Do other stuff
},
},
},
);
await User.create({}); // Runs the global hook
await Project.create({}); // Runs its own hook, followed by the global hook
永久钩子也可以在传递给 Sequelize 构造函数的选项中定义
new Sequelize(..., {
hooks: {
beforeCreate() {
// do stuff
}
}
});
请注意,以上内容与上面提到的默认钩子不同。后者使用构造函数的 define
选项。前者则没有。
连接钩子
Sequelize 提供了四个钩子,这些钩子在获取或释放数据库连接的立即之前和之后执行
sequelize.beforeConnect(callback)
- 回调的格式为
async (config) => /* ... */
- 回调的格式为
sequelize.afterConnect(callback)
- 回调的格式为
async (connection, config) => /* ... */
- 回调的格式为
sequelize.beforeDisconnect(callback)
- 回调的格式为
async (connection) => /* ... */
- 回调的格式为
sequelize.afterDisconnect(callback)
- 回调的格式为
async (connection) => /* ... */
- 回调的格式为
如果您需要异步获取数据库凭据,或者需要在创建低级数据库连接后直接访问它,这些钩子会很有用。
例如,我们可以从旋转令牌存储中异步获取数据库密码,并使用新凭据修改 Sequelize 的配置对象
sequelize.beforeConnect(async config => {
config.password = await getAuthToken();
});
您还可以使用两个在获取池连接的立即之前和之后执行的钩子
sequelize.beforePoolAcquire(callback)
- 回调的格式为
async (config) => /* ... */
- 回调的格式为
sequelize.afterPoolAcquire(callback)
- 回调的格式为
async (connection, config) => /* ... */
- 回调的格式为
这些钩子只能声明为永久全局钩子,因为连接池由所有模型共享。
实例钩子
以下钩子将在您编辑单个对象时发出
beforeValidate
afterValidate
/validationFailed
beforeCreate
/beforeUpdate
/beforeSave
/beforeDestroy
afterCreate
/afterUpdate
/afterSave
/afterDestroy
User.beforeCreate(user => {
if (user.accessLevel > 10 && user.username !== 'Boss') {
throw new Error("You can't grant this user an access level above 10!");
}
});
以下示例将抛出错误
try {
await User.create({ username: 'Not a Boss', accessLevel: 20 });
} catch (error) {
console.log(error); // You can't grant this user an access level above 10!
}
以下示例将成功
const user = await User.create({ username: 'Boss', accessLevel: 20 });
console.log(user); // user object with username 'Boss' and accessLevel of 20
模型钩子
有时您将通过使用 bulkCreate
、update
和 destroy
等方法一次编辑多条记录。以下钩子将在您使用其中一种方法时发出
YourModel.beforeBulkCreate(callback)
- 回调的格式为
(instances, options) => /* ... */
- 回调的格式为
YourModel.beforeBulkUpdate(callback)
- 回调的格式为
(options) => /* ... */
- 回调的格式为
YourModel.beforeBulkDestroy(callback)
- 回调的格式为
(options) => /* ... */
- 回调的格式为
YourModel.afterBulkCreate(callback)
- 回调的格式为
(instances, options) => /* ... */
- 回调的格式为
YourModel.afterBulkUpdate(callback)
- 回调的格式为
(options) => /* ... */
- 回调的格式为
YourModel.afterBulkDestroy(callback)
- 回调的格式为
(options) => /* ... */
- 回调的格式为
注意:像 bulkCreate
这样的方法默认情况下不会发出单个钩子 - 仅发出批量钩子。但是,如果您希望也发出单个钩子,则可以将 { individualHooks: true }
选项传递给查询调用。但是,这会极大地影响性能,具体取决于所涉及的记录数量(因为,除其他事项外,所有实例都将加载到内存中)。示例
await Model.destroy({
where: { accessLevel: 0 },
individualHooks: true,
});
// This will select all records that are about to be deleted and emit `beforeDestroy` and `afterDestroy` on each instance.
await Model.update(
{ username: 'Tony' },
{
where: { accessLevel: 0 },
individualHooks: true,
},
);
// This will select all records that are about to be updated and emit `beforeUpdate` and `afterUpdate` on each instance.
如果您使用 Model.bulkCreate(...)
和 updateOnDuplicate
选项,则钩子中对未在 updateOnDuplicate
数组中给出的字段所做的更改将不会持久保存到数据库中。但是,如果这是您想要的,则可以在钩子内部更改 updateOnDuplicate
选项。
User.beforeBulkCreate((users, options) => {
for (const user of users) {
if (user.isMember) {
user.memberSince = new Date();
}
}
// Add `memberSince` to updateOnDuplicate otherwise it won't be persisted
if (options.updateOnDuplicate && !options.updateOnDuplicate.includes('memberSince')) {
options.updateOnDuplicate.push('memberSince');
}
});
// Bulk updating existing users with updateOnDuplicate option
await Users.bulkCreate(
[
{ id: 1, isMember: true },
{ id: 2, isMember: false },
],
{
updateOnDuplicate: ['isMember'],
},
);
异常
只有模型方法会触发钩子。这意味着在某些情况下,Sequelize 将与数据库交互而不会触发钩子。这些包括但不限于
- 由于
ON DELETE CASCADE
约束,实例被数据库删除,除非hooks
选项为 true。 - 由于
SET NULL
或SET DEFAULT
约束,实例被数据库更新。 - 原始查询.
- 所有 QueryInterface 方法。
如果您需要对这些事件做出反应,请考虑使用数据库的本机触发器和通知系统。
级联删除的钩子
如异常中所述,当实例由于 ON DELETE CASCADE
约束而被数据库删除时,Sequelize 不会触发钩子。
但是,如果您在定义关联时将 hooks
选项设置为 true
,则 Sequelize 将为已删除的实例触发 beforeDestroy
和 afterDestroy
钩子。
不建议使用此选项,原因如下
- 此选项需要许多额外的查询。
destroy
方法通常执行单个查询。如果启用此选项,将执行一个额外的SELECT
查询,以及为 select 返回的每一行执行一个额外的DELETE
查询。 - 如果您没有在事务中运行此查询,并且发生错误,您可能会导致一些行被删除而另一些行未被删除。
- 此选项仅在使用
destroy
的实例版本时有效。静态版本不会触发钩子,即使使用individualHooks
也是如此。 - 此选项在
paranoid
模式下无效。 - 如果您只在拥有外键的模型上定义关联,则此选项无效。您还需要定义反向关联。
此选项被视为遗留选项。如果您需要在数据库更改时收到通知,我们强烈建议您使用数据库的触发器和通知系统。
以下是如何使用此选项的示例
import { Model } from 'sequelize';
const sequelize = new Sequelize({
/* options */
});
class User extends Model {}
User.init({}, { sequelize });
class Post extends Model {}
Post.init({}, { sequelize });
Post.beforeDestroy(() => {
console.log('Post has been destroyed');
});
// This "hooks" option will cause the "beforeDestroy" and "afterDestroy"
User.hasMany(Post, { onDelete: 'cascade', hooks: true });
await sequelize.sync({ force: true });
const user = await User.create();
const post = await Post.create({ userId: user.id });
// this will log "Post has been destroyed"
await user.destroy();
关联
在大多数情况下,钩子在关联实例时工作方式相同。
一对一和一对多关联
- 使用
add
/set
混合方法时,将运行beforeUpdate
和afterUpdate
钩子。
多对多关联
-
当使用
add
混合方法用于belongsToMany
关系(这会将一个或多个记录添加到连接表)时,连接模型中的beforeBulkCreate
和afterBulkCreate
钩子将运行。- 如果将
{ individualHooks: true }
传递给调用,则每个单独的钩子也将运行。
- 如果将
-
当使用
remove
混合方法用于belongsToMany
关系(这会从连接表中删除一个或多个记录)时,连接模型中的beforeBulkDestroy
和afterBulkDestroy
钩子将运行。- 如果将
{ individualHooks: true }
传递给调用,则每个单独的钩子也将运行。
- 如果将
如果您的关联是多对多,您可能希望在使用remove
调用时在中间模型上触发钩子。在内部,Sequelize 使用Model.destroy
导致调用bulkDestroy
而不是每个中间实例上的before/afterDestroy
钩子。
钩子和事务
Sequelize 中的许多模型操作允许您在方法的选项参数中指定事务。如果在原始调用中指定了事务,它将存在于传递给钩子函数的选项参数中。例如,考虑以下代码片段
User.addHook('afterCreate', async (user, options) => {
// We can use `options.transaction` to perform some other call
// using the same transaction of the call that triggered this hook
await User.update(
{ mood: 'sad' },
{
where: {
id: user.id,
},
transaction: options.transaction,
},
);
});
await sequelize.transaction(async t => {
await User.create(
{
username: 'someguy',
mood: 'happy',
},
{
transaction: t,
},
);
});
如果我们在前面的代码中没有在对User.update
的调用中包含事务选项,则不会发生任何更改,因为我们新创建的用户在挂起的事务提交之前不存在于数据库中。
内部事务
务必注意,Sequelize 可能会在内部使用事务来执行某些操作,例如Model.findOrCreate
。如果您的钩子函数执行依赖于对象在数据库中存在或修改对象存储值的读写操作(如上一节中的示例),则应始终指定{ transaction: options.transaction }
- 如果使用了事务,则
{ transaction: options.transaction }
将确保再次使用它; - 否则,
{ transaction: options.transaction }
将等效于{ transaction: undefined }
,它不会使用事务(这没问题)。
这样,您的钩子将始终表现正确。