フック
フック(ライフサイクルイベントとも呼ばれます)は、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を返す必要があります。
現在、プログラムでフックを追加する方法は3つあります。
// 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';
});
フックの削除
名前パラメータを持つフックのみ削除できます。
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は、データベース接続の取得または解放の前後にすぐに実行される4つのフックを提供します。
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();
});
プール接続の取得の前後にすぐに実行される2つのフックも使用できます。
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`制約のためにデータベースによってインスタンスが更新される場合。
- Rawクエリ.
- すべてのQueryInterfaceメソッド。
これらのイベントに対応する必要がある場合は、代わりにデータベースのネイティブなトリガーと通知システムを使用することを検討してください。
カスケード削除のフック
例外で示されているように、`ON DELETE CASCADE`制約のためにデータベースによってインスタンスが削除される場合、Sequelizeはフックをトリガーしません。
ただし、アソシエーションを定義するときに`hooks`オプションを`true`に設定すると、削除されたインスタンスに対して`beforeDestroy`と`afterDestroy`フックがトリガーされます。
このオプションの使用は、次の理由により推奨されません。
- このオプションでは、多くの追加クエリが必要です。`destroy`メソッドは通常、単一のクエリを実行します。このオプションが有効になっている場合、SELECTクエリ1つと、selectによって返された各行に対するDELETEクエリ1つが追加で実行されます。
- トランザクション内でこのクエリを実行せず、エラーが発生した場合、一部の行が削除され、一部の行が削除されないままになる可能性があります。
- このオプションは、`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();
アソシエーション
ほとんどの場合、フックは関連付けられているインスタンスに対して同じように機能します。
1対1および1対多のアソシエーション
add
/set
ミックスインメソッドを使用する場合、beforeUpdate
およびafterUpdate
フックが実行されます。
多対多のアソシエーション
-
belongsToMany
リレーションシップに対してadd
ミックスインメソッドを使用する場合(ジャンクションテーブルに1つ以上のレコードを追加する場合)、ジャンクションモデルのbeforeBulkCreate
およびafterBulkCreate
フックが実行されます。{ individualHooks: true }
が呼び出しに渡された場合、各個々のフックも実行されます。
-
belongsToMany
リレーションシップに対してremove
ミックスインメソッドを使用する場合(ジャンクションテーブルから1つ以上のレコードを削除する場合)、ジャンクションモデルのbeforeBulkDestroy
およびafterBulkDestroy
フックが実行されます。{ individualHooks: true }
が呼び出しに渡された場合、各個々のフックも実行されます。
アソシエーションが多対多の場合、remove
呼び出し時にスルーモデルでフックを実行することに関心があるかもしれません。内部的に、Sequelize はModel.destroy
を使用しており、各スルーインスタンスのbefore/afterDestroy
フックではなくbulkDestroy
を呼び出します。
フックとトランザクション
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 }
と同等になり、トランザクションを使用しません(問題ありません)。
このようにして、フックは常に正しく動作します。