钩子
钩子(也称为生命周期事件)是在执行 sequelize 中的调用之前和之后调用的函数。例如,如果你希望在保存模型之前始终设置模型的值,则可以添加 beforeUpdate
钩子。
¥Hooks (also known as lifecycle events), are functions which are called before and after calls in sequelize are executed. For example, if you want to always set a value on a model before saving it, you can add a beforeUpdate
hook.
注意:你不能将钩子与实例一起使用。钩子与模型一起使用。
¥Note: You can't use hooks with instances. Hooks are used with models.
可用钩子
¥Available hooks
Sequelize 提供了很多 hooks。完整列表可以直接在 源代码 - src/hooks.js。
¥Sequelize provides a lot of hooks. The full list can be found in directly in the source code - src/hooks.js.
钩子触发顺序
¥Hooks firing order
下图显示了最常见钩子的触发顺序。
¥The diagram below shows the firing order for the most common hooks.
注意:该列表并不详尽。
¥Note: this list is not exhaustive.
(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)
声明钩子
¥Declaring Hooks
钩子的参数通过引用传递。这意味着你可以更改值,这将反映在插入/更新语句中。钩子可能包含异步操作 - 在这种情况下,钩子函数应该返回一个 promise。
¥Arguments to hooks are passed by reference. This means, that you can change the values, and this will be reflected in the insert / update statement. A hook may contain async actions - in this case the hook function should return a promise.
目前有三种方式可以通过编程方式添加钩子:
¥There are currently three ways to programmatically add hooks:
// 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';
});
拆除钩子
¥Removing hooks
只能删除带有 name 参数的钩子。
¥Only a hook with name param can be removed.
class Book extends Model {}
Book.init(
{
title: DataTypes.STRING,
},
{ sequelize },
);
Book.addHook('afterCreate', 'notifyUsers', (book, options) => {
// ...
});
Book.removeHook('afterCreate', 'notifyUsers');
你可以有许多同名的钩子。调用 .removeHook()
将删除所有这些。
¥You can have many hooks with same name. Calling .removeHook()
will remove all of them.
全局/通用钩子
¥Global / universal hooks
全局钩子是为所有模型运行的钩子。它们对于插件特别有用,可以定义你想要的所有模型的行为,例如允许在模型上使用 sequelize.define
自定义时间戳:
¥Global hooks are hooks that are run for all models. They are especially useful for plugins and can define behaviours that you want for all your models, for example to allow customization on timestamps using sequelize.define
on your models:
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, '');
},
},
},
);
它们可以通过多种方式定义,其语义略有不同:
¥They can be defined in many ways, which have slightly different semantics:
默认 Hooks(在 Sequelize 构造函数选项上)
¥Default Hooks (on Sequelize constructor options)
const sequelize = new Sequelize(..., {
define: {
hooks: {
beforeCreate() {
// Do stuff
}
}
}
});
这会向所有模型添加一个默认钩子,如果模型没有定义自己的 beforeCreate
钩子,则会运行该默认钩子:
¥This adds a default hook to all models, which is run if the model does not define its own beforeCreate
hook:
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
)
¥Permanent Hooks (with sequelize.addHook
)
sequelize.addHook('beforeCreate', () => {
// Do stuff
});
无论模型是否指定其自己的 beforeCreate
钩子,此钩子始终运行。本地钩子总是在全局钩子之前运行:
¥This hook is always run, whether or not the model specifies its own beforeCreate
hook. Local hooks are always run before global hooks:
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 构造函数的选项中定义:
¥Permanent hooks may also be defined in the options passed to the Sequelize constructor:
new Sequelize(..., {
hooks: {
beforeCreate() {
// do stuff
}
}
});
注意,上面的和上面提到的 Default Hooks 不一样。该函数使用构造函数的 define
选项。这个没有。
¥Note that the above is not the same as the Default Hooks mentioned above. That one uses the define
option of the constructor. This one does not.
连接钩子
¥Connection Hooks
Sequelize 提供了四个钩子,它们在获取或释放数据库连接之前和之后立即执行:
¥Sequelize provides four hooks that are executed immediately before and after a database connection is obtained or released:
-
sequelize.beforeConnect(callback)
-
回调的形式为
async (config) => /* ... */
¥The callback has the form
async (config) => /* ... */
-
-
sequelize.afterConnect(callback)
-
回调的形式为
async (connection, config) => /* ... */
¥The callback has the form
async (connection, config) => /* ... */
-
-
sequelize.beforeDisconnect(callback)
-
回调的形式为
async (connection) => /* ... */
¥The callback has the form
async (connection) => /* ... */
-
-
sequelize.afterDisconnect(callback)
-
回调的形式为
async (connection) => /* ... */
¥The callback has the form
async (connection) => /* ... */
-
如果你需要异步获取数据库凭据,或者需要在创建底层数据库连接后直接访问该连接,这些钩子可能会很有用。
¥These hooks can be useful if you need to asynchronously obtain database credentials, or need to directly access the low-level database connection after it has been created.
例如,我们可以从旋转令牌存储中异步获取数据库密码,并使用新凭据更改 Sequelize 的配置对象:
¥For example, we can asynchronously obtain a database password from a rotating token store, and mutate Sequelize's configuration object with the new credentials:
sequelize.beforeConnect(async config => {
config.password = await getAuthToken();
});
你还可以使用在获取池连接之前和之后立即执行的两个钩子:
¥You can also use two hooks that are executed immediately before and after a pool connection is acquired:
-
sequelize.beforePoolAcquire(callback)
-
回调的形式为
async (config) => /* ... */
¥The callback has the form
async (config) => /* ... */
-
-
sequelize.afterPoolAcquire(callback)
-
回调的形式为
async (connection, config) => /* ... */
¥The callback has the form
async (connection, config) => /* ... */
-
这些钩子只能声明为永久全局钩子,因为连接池由所有模型共享。
¥These hooks may only be declared as a permanent global hook, as the connection pool is shared by all models.
实例钩子
¥Instance hooks
每当你编辑单个对象时,都会触发以下钩子:
¥The following hooks will emit whenever you're editing a single object:
-
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!");
}
});
以下示例将引发错误:
¥The following example will throw an error:
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!
}
下面的例子就会成功:
¥The following example will be successful:
const user = await User.create({ username: 'Boss', accessLevel: 20 });
console.log(user); // user object with username 'Boss' and accessLevel of 20
模型钩子
¥Model hooks
有时,你会使用 bulkCreate
、update
和 destroy
等方法一次编辑多个记录。每当你使用其中一种方法时,都会触发以下钩子:
¥Sometimes you'll be editing more than one record at a time by using methods like bulkCreate
, update
and destroy
. The following hooks will emit whenever you're using one of those methods:
-
YourModel.beforeBulkCreate(callback)
-
回调的形式为
(instances, options) => /* ... */
¥The callback has the form
(instances, options) => /* ... */
-
-
YourModel.beforeBulkUpdate(callback)
-
回调的形式为
(options) => /* ... */
¥The callback has the form
(options) => /* ... */
-
-
YourModel.beforeBulkDestroy(callback)
-
回调的形式为
(options) => /* ... */
¥The callback has the form
(options) => /* ... */
-
-
YourModel.afterBulkCreate(callback)
-
回调的形式为
(instances, options) => /* ... */
¥The callback has the form
(instances, options) => /* ... */
-
-
YourModel.afterBulkUpdate(callback)
-
回调的形式为
(options) => /* ... */
¥The callback has the form
(options) => /* ... */
-
-
YourModel.afterBulkDestroy(callback)
-
回调的形式为
(options) => /* ... */
¥The callback has the form
(options) => /* ... */
-
注意:默认情况下,像 bulkCreate
这样的方法不会触发单独的钩子 - 只有散装钩子。但是,如果你还希望触发单个钩子,则可以将 { individualHooks: true }
选项传递给查询调用。但是,这可能会极大地影响性能,具体取决于所涉及的记录数量(因为除其他外,所有实例都将加载到内存中)。示例:
¥Note: methods like bulkCreate
do not emit individual hooks by default - only the bulk hooks. However, if you want individual hooks to be emitted as well, you can pass the { individualHooks: true }
option to the query call. However, this can drastically impact performance, depending on the number of records involved (since, among other things, all instances will be loaded into memory). Examples:
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
选项。
¥If you use Model.bulkCreate(...)
with the updateOnDuplicate
option, changes made in the hook to fields that aren't given in the updateOnDuplicate
array will not be persisted to the database. However it is possible to change the updateOnDuplicate
option inside the hook if this is what you want.
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'],
},
);
异常
¥Exceptions
只有模型方法才会触发钩子。这意味着在很多情况下 Sequelize 将与数据库交互而不触发钩子。这些包括但不限于:
¥Only Model methods trigger hooks. This means there are a number of cases where Sequelize will interact with the database without triggering hooks. These include but are not limited to:
-
由于
ON DELETE CASCADE
约束、除非hooks
选项为 true 被数据库删除的实例。¥Instances being deleted by the database because of an
ON DELETE CASCADE
constraint, except if thehooks
option is true. -
由于
SET NULL
或SET DEFAULT
约束,数据库正在更新实例。¥Instances being updated by the database because of a
SET NULL
orSET DEFAULT
constraint. -
原始查询。
-
所有 QueryInterface 方法。
¥All QueryInterface methods.
如果你需要对这些事件做出反应,请考虑使用数据库的原生触发器和通知系统。
¥If you need to react to these events, consider using your database's native triggers and notification system instead.
用于级联删除的钩子
¥Hooks for cascade deletes
如 异常 所示,当数据库因 ON DELETE CASCADE
约束而删除实例时,Sequelize 不会触发钩子。
¥As indicated in Exceptions, Sequelize will not trigger hooks when instances are deleted by the database because of an ON DELETE CASCADE
constraint.
但是,如果你在定义关联时将 hooks
选项设置为 true
,Sequelize 将触发已删除实例的 beforeDestroy
和 afterDestroy
钩子。
¥However, if you set the hooks
option to true
when defining your association, Sequelize will trigger the beforeDestroy
and afterDestroy
hooks for the deleted instances.
不鼓励使用此选项,原因如下:
¥Using this option is discouraged for the following reasons:
-
此选项需要许多额外的查询。
destroy
方法通常执行单个查询。如果启用此选项,则会对 select 返回的每一行执行额外的SELECT
查询以及额外的DELETE
查询。¥This option requires many extra queries. The
destroy
method normally executes a single query. If this option is enabled, an extraSELECT
query, as well as an extraDELETE
query for each row returned by the select will be executed. -
如果你不在事务中运行此查询,并且发生错误,则最终可能会导致某些行被删除,而某些行则未被删除。
¥If you do not run this query in a transaction, and an error occurs, you may end up with some rows deleted and some not deleted.
-
该选项仅在实例版本为
destroy
时有效。即使使用individualHooks
,静态版本也不会触发钩子。¥This option only works when the instance version of
destroy
is used. The static version will not trigger the hooks, even withindividualHooks
. -
该选项在
paranoid
模式下不起作用。¥This option will not work in
paranoid
mode. -
如果你仅在拥有外键的模型上定义关联,则此选项将不起作用。你还需要定义反向关联。
¥This option will not work if you only define the association on the model that owns the foreign key. You need to define the reverse association as well.
此选项被视为旧版选项。如果你需要收到数据库更改的通知,我们强烈建议你使用数据库的触发器和通知系统。
¥This option is considered legacy. We highly recommend using your database's triggers and notification system if you need to be notified of database changes.
以下是如何使用此选项的示例:
¥Here is an example of how to use this option:
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();
关联
¥Associations
大多数情况下,钩子在关联时对于实例的工作方式相同。
¥For the most part hooks will work the same for instances when being associated.
一对一和一对多关联
¥One-to-One and One-to-Many associations
-
当使用
add
/set
mixin 方法时,beforeUpdate
和afterUpdate
钩子将运行。¥When using
add
/set
mixin methods thebeforeUpdate
andafterUpdate
hooks will run.
多对多关联
¥Many-to-Many associations
-
当对
belongsToMany
关系使用add
mixin 方法(将向联结表添加一条或多条记录)时,联结模型中的beforeBulkCreate
和afterBulkCreate
钩子将运行。¥When using
add
mixin methods forbelongsToMany
relationships (that will add one or more records to the junction table) thebeforeBulkCreate
andafterBulkCreate
hooks in the junction model will run.-
如果
{ individualHooks: true }
被传递给调用,则每个单独的钩子也将运行。¥If
{ individualHooks: true }
was passed to the call, then each individual hook will also run.
-
-
当对
belongsToMany
关系使用remove
mixin 方法时(这会将一条或多条记录删除到联结表中),联结模型中的beforeBulkDestroy
和afterBulkDestroy
钩子将运行。¥When using
remove
mixin methods forbelongsToMany
relationships (that will remove one or more records to the junction table) thebeforeBulkDestroy
andafterBulkDestroy
hooks in the junction model will run.-
如果
{ individualHooks: true }
被传递给调用,则每个单独的钩子也将运行。¥If
{ individualHooks: true }
was passed to the call, then each individual hook will also run.
-
如果你的关联是多对多,你可能有兴趣在使用 remove
调用时在直通模型上触发钩子。在内部,sequelize 使用 Model.destroy
导致在每个 through 实例上调用 bulkDestroy
而不是 before/afterDestroy
钩子。
¥If your association is Many-to-Many, you may be interested in firing hooks on the through model when using the remove
call. Internally, sequelize is using Model.destroy
resulting in calling the bulkDestroy
instead of the before/afterDestroy
hooks on each through instance.
钩子和事务
¥Hooks and Transactions
Sequelize 中的许多模型操作允许你在方法的 options 参数中指定事务。如果在原始调用中指定了事务,它将出现在传递给钩子函数的选项参数中。例如,考虑以下代码片段:
¥Many model operations in Sequelize allow you to specify a transaction in the options parameter of the method. If a transaction is specified in the original call, it will be present in the options parameter passed to the hook function. For example, consider the following snippet:
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
的调用中没有包含事务选项,则不会发生任何更改,因为在提交挂起的事务之前,我们新创建的用户并不存在于数据库中。
¥If we had not included the transaction option in our call to User.update
in the preceding code, no change would have occurred, since our newly created user does not exist in the database until the pending transaction has been committed.
内部事务
¥Internal Transactions
认识到 Sequelize 可能会在内部使用事务来执行某些操作(例如 Model.findOrCreate
)是非常重要的。如果你的钩子函数执行依赖于数据库中对象存在的读或写操作,或者修改对象的存储值(如上一节中的示例),则应始终指定 { transaction: options.transaction }
:
¥It is very important to recognize that sequelize may make use of transactions internally for certain operations such as Model.findOrCreate
. If your hook functions execute read or write operations that rely on the object's presence in the database, or modify the object's stored values like the example in the preceding section, you should always specify { transaction: options.transaction }
:
-
如果事务被使用过,那么
{ transaction: options.transaction }
将确保它被再次使用;¥If a transaction was used, then
{ transaction: options.transaction }
will ensure it is used again; -
否则,
{ transaction: options.transaction }
将相当于{ transaction: undefined }
,它不会使用事务(这是可以的)。¥Otherwise,
{ transaction: options.transaction }
will be equivalent to{ transaction: undefined }
, which won't use a transaction (which is ok).
这样你的钩子将始终表现正确。
¥This way your hooks will always behave correctly.