docs/other-topics/hooks.mdx
import DecoratorInfo from '../_fragments/_decorator-info.mdx';
Hooks are events that you can listen to, and which are triggerred when their corresponding method is called.
They are useful for adding custom functionality to the core of the application.
For example, if you want to always set a value on a model before saving it, you can listen to the Model beforeUpdate hook.
The Sequelize class supports the following hooks:
| Hook name | Async | Run when |
|---|---|---|
beforeInit, afterInit | ❌ | A sequelize instance is created |
Example:
import { Sequelize } from '@sequelize/core';
Sequelize.hooks.addListener('beforeInit', () => {
console.log('A new sequelize instance is being created');
});
Sequelize instances support the following hooks:
| Hook name | Async | Run when |
|---|---|---|
beforeDefine, afterDefine | ❌ | A new Model class is being registered |
beforeQuery, afterQuery | ✅ | A SQL query is being run |
beforeBulkSync, afterBulkSync | ✅ | sequelize.sync is called |
beforeConnect, afterConnect | ✅ | Whenever a new connection to the database is being created |
beforeDisconnect, afterDisconnect | ✅ | Whenever a connection to the database is being closed |
beforePoolAcquire, afterPoolAcquire | ✅ | Whenever a new connection from pool is being acquired |
:::info
All Model hooks are also fired on the sequelize instance:
import { Sequelize } from '@sequelize/core';
const sequelize = new Sequelize(/* options */);
// This will be called whenever findAll is called on any model.
sequelize.hooks.addListener('beforeFind', () => {
console.log('findAll has been called a model');
});
:::
Instance Sequelize hooks can be registered in two ways:
Using the hooks property of the Sequelize instance:
import { Sequelize } from '@sequelize/core';
const sequelize = new Sequelize(/* options */);
// highlight-next-line
sequelize.hooks.addListener('beforeDefine', () => {
console.log('A new Model is being initialized');
});
Through the Sequelize options:
import { Sequelize } from '@sequelize/core';
const sequelize = new Sequelize({
/* options */
// highlight-next-line
hooks: {
beforeDefine: () => {
console.log('A new Model is being initialized');
},
},
});
Model classes support the following hooks:
| Hook name | Async | Run when |
|---|---|---|
beforeAssociate, afterAssociate | ❌ | Whenever an association is declared on the model |
beforeSync, afterSync | ✅ | When sequelize.sync or Model.sync are called |
beforeValidate, afterValidate, validationFailed | ✅ | When the model's attributes are being validated (happens in most model methods) |
beforeFind, beforeFindAfterExpandIncludeAll, beforeFindAfterOptions, afterFind | ✅ | When Model.findAll is called1 |
beforeCount | ✅ | When Model.count is called |
beforeUpsert, afterUpsert | ✅ | When Model.upsert is called |
beforeBulkCreate, afterBulkCreate | ✅ | When Model.bulkCreate is called |
beforeBulkDestroy, afterBulkDestroy | ✅ | When Model.destroy is called |
beforeDestroy, afterDestroy | ✅ | When Model#destroy is called |
beforeBulkRestore, afterBulkRestore | ✅ | When Model.restore is called |
beforeRestore, afterRestore | ✅ | When Model#restore is called |
beforeBulkUpdate, afterBulkUpdate | ✅ | When Model.update is called |
beforeUpdate, afterUpdate | ✅ | When Model#update or Model#save is called (and the model isn't new) |
beforeCreate, afterCreate | ✅ | When Model#save is called (and the model is new) |
beforeSave, afterSave | ✅ | When Model#update or Model#save is called |
Instance Sequelize hooks can be registered in three ways:
Using the hooks property of the Sequelize instance:
import { Sequelize, DataTypes } from '@sequelize/core';
const sequelize = new Sequelize(/* options */);
const MyModel = sequelize.define('MyModel', {
name: DataTypes.STRING,
});
// highlight-next-line
MyModel.hooks.addListener('beforeFind', () => {
console.log('findAll has been called on MyModel');
});
Through the options of Model.init, or sequelize.define:
import { DataTypes } from '@sequelize/core';
const MyModel = sequelize.define(
'MyModel',
{
name: DataTypes.STRING,
},
{
// highlight-next-line
hooks: {
beforeFind: () => {
console.log('findAll has been called on MyModel');
},
},
},
);
Using decorators.
There is a decorator for every model hook, their names are the same as the hook names, but in PascalCase.
import { Sequelize, Model, Hook } from '@sequelize/core';
import { BeforeFind } from '@sequelize/core/decorators-legacy';
export class MyModel extends Model {
// highlight-next-line
@BeforeFind
static logFindAll() {
console.log('findAll has been called on MyModel');
}
}
In the tables above, you can see that some hooks are marked as Async.
This means that your listener can return a Promise that will be awaited before continuing with the execution of the hook.
Be careful about doing async operations in hooks. Hooks are run in series and will not run the next hook until your hook has resolved. This can cause performance issues if you have a lot of hooks.
Trying to return a Promise from a synchronous hook will raise an error.
You can remove hooks using hooks.removeListener and providing either the same callback as hooks.addListener or the name of your listener:
class Book extends Model {}
Book.init(
{
title: DataTypes.STRING,
},
{ sequelize },
);
const myListener = (book, options) => {
// ...
};
Book.hooks.addListener('afterCreate', 'yourHookIdentifier', myListener);
// Both of these will remove the hook:
// success-next-line
Book.hooks.removeListener('afterCreate', myListener);
// success-next-line
Book.hooks.removeListener('afterCreate', 'yourHookIdentifier');
:::caution
Hooks added by decorators cannot be removed using the callback instance, but can still be removed if they are named:
import { Sequelize, Model, Hook } from '@sequelize/core';
import { BeforeFind } from '@sequelize/core/decorators-legacy';
export class MyModel extends Model {
@BeforeFind({ name: 'yourHookIdentifier' })
static logFindAll() {
console.log('findAll has been called on MyModel');
}
}
// This will not work
// error-next-line
MyModel.hooks.removeListener('beforeFind', MyModel.logFindAll);
// But this will
// success-next-line
MyModel.hooks.removeListener('beforeFind', 'yourHookIdentifier');
However, we do not recommend removing hooks added through decorators, as it may make your code harder to understand.
:::
Methods added by Associations on your model do provide hooks specific to them, but they are built on top of regular model methods, which will trigger hooks.
For instance, using the add / set mixin methods will trigger the beforeUpdate and afterUpdate hooks.
individualHooks:::caution
Using this option is discouraged for two reasons:
individualHooks cannot be modified. Only the "bulk" event's data can be modified.Use with care. If you need to react to database changes, consider using your database's triggers and notification system instead.
:::
When using static model methods such as Model.destroy or Model.update, only their corresponding "bulk" hook (such as beforeBulkDestroy) will be called, not the instance hooks (such as beforeDestroy).
If you want to trigger the instance hooks, you use the individualHooks option to the method to run the instance hooks for each row that will be impacted.
The following example will trigger the beforeDestroy and afterDestroy hooks for each row that will be deleted:
User.destroy({
where: {
id: [1, 2, 3],
},
individualHooks: true,
});
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 constraint, except if the hooks option is true.SET NULL or SET DEFAULT constraint.If you need to react to these events, consider using your database's native and notification system instead.
As indicated in Exceptions, Sequelize will not trigger hooks when instances are deleted by the database because of an ON DELETE CASCADE constraint.
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.
:::caution
Using this option is discouraged for the following reasons:
destroy method normally executes a single query.
If this option is enabled, an extra SELECT query, as well as an extra DELETE query for each row returned by the select will be executed.destroy is used. The static version will not trigger the hooks, even with individualHooks.paranoid mode.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/core';
import { HasMany, BeforeDestroy } from '@sequelize/core/decorators-legacy';
class User extends Model {
// This "hooks" option will cause the "beforeDestroy" and "afterDestroy"
// highlight-next-line
@HasMany(() => Post, { hooks: true })
declare posts: Post[];
}
class Post extends Model {
@BeforeDestroy
static logDestroy() {
console.log('Post has been destroyed');
}
}
const sequelize = new Sequelize({
/* options */
models: [User, Post],
});
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();
Many model operations in Sequelize support specifying 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.hooks.addListener('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,
},
// highlight-next-line
transaction: options.transaction,
},
);
});
await sequelize.transaction(async transaction => {
await User.create(
{
username: 'someguy',
mood: 'happy',
},
{
transaction,
},
);
});
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.
findAll: Note that some methods, such as Model.findOne, Model.findAndCountAll and association getters will also call Model.findAll internally. This will cause the beforeFind hook to be called for these methods too. ↩