跳至主要內容
版本:v6 - 穩定版

鉤子

鉤子(也稱為生命週期事件)是在 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

模型鉤子

有時您會使用像 bulkCreateupdatedestroy 之類的方法一次編輯多個記錄。當您使用這些方法之一時,將會發出下列鉤子

  • 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 NULLSET DEFAULT 約束而由資料庫更新的實例。
  • 原始查詢.
  • 所有 QueryInterface 方法。

如果您需要對這些事件做出反應,請考慮改為使用資料庫的原生觸發器和通知系統。

用於串聯刪除的鉤子

例外狀況 中所述,當實例由於 ON DELETE CASCADE 約束而由資料庫刪除時,Sequelize 不會觸發鉤子。

但是,如果您在定義關聯時將 hooks 選項設定為 true,Sequelize 將會針對已刪除的實例觸發 beforeDestroyafterDestroy 鉤子。

警告

由於以下原因,不建議使用此選項

  • 此選項需要許多額外的查詢。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 混入方法時,beforeUpdateafterUpdate 鉤子將會執行。

多對多關聯

  • 當對 belongsToMany 關係使用 add 混入方法(這會將一或多筆記錄加入到連接表)時,連接模型中的 beforeBulkCreateafterBulkCreate 鉤子將會執行。

    • 如果傳遞了 { individualHooks: true } 給呼叫,則每個個別的鉤子也會執行。
  • 當對 belongsToMany 關係使用 remove 混入方法(這會將一或多筆記錄從連接表中移除)時,連接模型中的 beforeBulkDestroyafterBulkDestroy 鉤子將會執行。

    • 如果傳遞了 { 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 },這不會使用交易(這是可以的)。

這樣您的鉤子將始終正常運作。