Hooks
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.
Static Sequelize Hooks
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');
});
Instance Sequelize Hooks
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 |
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');
});
Registering Sequelize hooks
Instance Sequelize hooks can be registered in two ways:
-
Using the
hooks
property of theSequelize
instance:import { Sequelize } from '@sequelize/core';
const sequelize = new Sequelize(/* options */);
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 */
hooks: {
beforeDefine: () => {
console.log('A new Model is being initialized');
},
},
});
Model Sequelize Hooks
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 |
Registering Model hooks
Instance Sequelize hooks can be registered in three ways:
-
Using the
hooks
property of theSequelize
instance:import { Sequelize, DataTypes } from '@sequelize/core';
const sequelize = new Sequelize(/* options */);
const MyModel = sequelize.define('MyModel', {
name: DataTypes.STRING,
});
MyModel.hooks.addListener('beforeFind', () => {
console.log('findAll has been called on MyModel');
}); -
Through the options of
Model.init
, orsequelize.define
:import { DataTypes } from '@sequelize/core';
const MyModel = sequelize.define(
'MyModel',
{
name: DataTypes.STRING,
},
{
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 inPascalCase
.infoSequelize currently only supports the legacy/experimental decorator format. Support for the new decorator format will be added in a future release.
All decorators must be imported from
@sequelize/core/decorators-legacy
:import { Attribute, Table } from '@sequelize/core/decorators-legacy';
Using legacy decorators requires to use a transpiler such as TypeScript, Babel or others to compile them to JavaScript. Alternatively, Sequelize also supports a legacy approach that does not require using decorators, but this is discouraged.
If you're using TypeScript, you will also need to configure it to be able to resolve this export, see our Getting Started guide for more information.
import { Sequelize, Model, Hook } from '@sequelize/core';
import { BeforeFind } from '@sequelize/core/decorators-legacy';
export class MyModel extends Model {
@BeforeFind
static logFindAll() {
console.log('findAll has been called on MyModel');
}
}
Sync vs Async Hooks
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.
Removing hooks
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:
Book.hooks.removeListener('afterCreate', myListener);
Book.hooks.removeListener('afterCreate', 'yourHookIdentifier');
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
MyModel.hooks.removeListener('beforeFind', MyModel.logFindAll);
// But this will
MyModel.hooks.removeListener('beforeFind', 'yourHookIdentifier');
However, we do not recommend removing hooks added through decorators, as it may make your code harder to understand.
Associations Method Hooks
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
Using this option is discouraged for two reasons:
- Unlike the "normal" hook, the data provided to events emitted by
individualHooks
cannot be modified. Only the "bulk" event's data can be modified. - This option is slow, as it will need to fetch the instances that need to be destroyed.
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,
});
Exceptions
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:
- Instances being deleted by the database because of an
ON DELETE CASCADE
constraint, except if thehooks
option is true. - Instances being updated by the database because of a
SET NULL
orSET DEFAULT
constraint. - Raw queries.
- All QueryInterface methods.
If you need to react to these events, consider using your database's native and notification system instead.
Hooks for cascade deletes
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.
Using this option is discouraged for the following reasons:
- 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.
- This option only works when the instance version of
destroy
is used. The static version will not trigger the hooks, even withindividualHooks
. - 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/core';
import { HasMany, BeforeDestroy } from '@sequelize/core/decorators-legacy';
class User extends Model {
// This "hooks" option will cause the "beforeDestroy" and "afterDestroy"
@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();
Hooks and Transactions
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,
},
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.
Footnotes
-
findAll: Note that some methods, such as
Model.findOne
,Model.findAndCountAll
and association getters will also callModel.findAll
internally. This will cause thebeforeFind
hook to be called for these methods too. ↩