Skip to main content
Version: v7 - alpha

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 nameAsyncRun when
beforeInit, afterInitA 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 nameAsyncRun when
beforeDefine, afterDefineA new Model class is being registered
beforeQuery, afterQueryA SQL query is being run
beforeBulkSync, afterBulkSyncsequelize.sync is called
beforeConnect, afterConnectWhenever a new connection to the database is being created
beforeDisconnect, afterDisconnectWhenever a connection to the database is being closed
beforePoolAcquire, afterPoolAcquireWhenever 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');
});

Registering Sequelize hooks

Instance Sequelize hooks can be registered in two ways:

  1. Using the hooks property of the Sequelize instance:

    import { Sequelize } from '@sequelize/core';

    const sequelize = new Sequelize(/* options */);

    sequelize.hooks.addListener('beforeDefine', () => {
    console.log('A new Model is being initialized');
    });
  2. 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 nameAsyncRun when
beforeAssociate, afterAssociateWhenever an association is declared on the model
beforeSync, afterSyncWhen sequelize.sync or Model.sync are called
beforeValidate, afterValidate, validationFailedWhen the model's attributes are being validated (happens in most model methods)
beforeFind, beforeFindAfterExpandIncludeAll, beforeFindAfterOptions, afterFindWhen Model.findAll is called1
beforeCountWhen Model.count is called
beforeUpsert, afterUpsertWhen Model.upsert is called
beforeBulkCreate, afterBulkCreateWhen Model.bulkCreate is called
beforeBulkDestroy, afterBulkDestroyWhen Model.destroy is called
beforeDestroy, afterDestroyWhen Model#destroy is called
beforeBulkRestore, afterBulkRestoreWhen Model.restore is called
beforeRestore, afterRestoreWhen Model#restore is called
beforeBulkUpdate, afterBulkUpdateWhen Model.update is called
beforeUpdate, afterUpdateWhen Model#update or Model#save is called (and the model isn't new)
beforeCreate, afterCreateWhen Model#save is called (and the model is new)
beforeSave, afterSaveWhen Model#update or Model#save is called

Registering Model hooks

Instance Sequelize hooks can be registered in three ways:

  1. 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,
    });

    MyModel.hooks.addListener('beforeFind', () => {
    console.log('findAll has been called on MyModel');
    });
  2. Through the options of Model.init, or sequelize.define:

    import { DataTypes } from '@sequelize/core';

    const MyModel = sequelize.define(
    'MyModel',
    {
    name: DataTypes.STRING,
    },
    {
    hooks: {
    beforeFind: () => {
    console.log('findAll has been called on MyModel');
    },
    },
    },
    );
  3. Using decorators.
    There is a decorator for every model hook, their names are the same as the hook names, but in PascalCase.

    info

    Sequelize 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');
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
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

caution

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 the hooks option is true.
  • Instances being updated by the database because of a SET NULL or SET 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.

caution

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 extra SELECT query, as well as an extra DELETE 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 with individualHooks.
  • 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

  1. 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.