鉤子
鉤子(也稱為生命週期事件)是在 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)
宣告鉤子
傳遞給鉤子的引數是透過參照傳遞的。這表示您可以變更值,而這會反映在 insert/update 陳述式中。鉤子可能包含非同步動作 - 在這種情況下,鉤子函式應傳回 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.
如果您使用帶有 updateOnDuplicate
選項的 Model.bulkCreate(...)
,在鉤子中對未在 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
鉤子將會執行。
多對多關聯
-
當對
belongsToMany
關係使用add
混入方法(這會將一或多筆記錄加入到連接表)時,連接模型中的beforeBulkCreate
和afterBulkCreate
鉤子將會執行。- 如果傳遞了
{ individualHooks: true }
給呼叫,則每個個別的鉤子也會執行。
- 如果傳遞了
-
當對
belongsToMany
關係使用remove
混入方法(這會將一或多筆記錄從連接表中移除)時,連接模型中的beforeBulkDestroy
和afterBulkDestroy
鉤子將會執行。- 如果傳遞了
{ individualHooks: true }
給呼叫,則每個個別的鉤子也會執行。
- 如果傳遞了
如果您的關聯是多對多的,您可能會對在透過模型上使用 remove
呼叫時觸發鉤子感興趣。在內部,Sequelize 使用 Model.destroy
,這會導致呼叫 bulkDestroy
,而不是每個透過實例上的 before/afterDestroy
鉤子。
鉤子和交易
Sequelize 中的許多模型操作允許您在方法的 options 參數中指定交易。如果原始呼叫中有指定交易,它將會出現在傳遞給鉤子函數的 options 參數中。例如,考慮以下程式碼片段
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 }
,這不會使用交易(這是可以的)。
這樣您的鉤子將始終正常運作。