Transactions

Sequelize does not use transactions by default. However, for production-ready usage of Sequelize, you should definitely configure Sequelize to use transactions.

Sequelize supports two ways of using transactions:

  1. Unmanaged transactions: Committing and rolling back the transaction should be done manually by the user (by calling the appropriate Sequelize methods).

  2. Managed transactions: Sequelize will automatically rollback the transaction if any error is thrown, or commit the transaction otherwise. Also, if CLS (Continuation Local Storage) is enabled, all queries within the transaction callback will automatically receive the transaction object.

Unmanaged transactions

Let’s start with an example:

  1. // First, we start a transaction and save it into a variable
  2. const t = await sequelize.transaction();
  3. try {
  4. // Then, we do some calls passing this transaction as an option:
  5. const user = await User.create({
  6. firstName: 'Bart',
  7. lastName: 'Simpson'
  8. }, { transaction: t });
  9. await user.addSibling({
  10. firstName: 'Lisa',
  11. lastName: 'Simpson'
  12. }, { transaction: t });
  13. // If the execution reaches this line, no errors were thrown.
  14. // We commit the transaction.
  15. await t.commit();
  16. } catch (error) {
  17. // If the execution reaches this line, an error was thrown.
  18. // We rollback the transaction.
  19. await t.rollback();
  20. }

As shown above, the unmanaged transaction approach requires that you commit and rollback the transaction manually, when necessary.

Managed transactions

Managed transactions handle committing or rolling back the transaction automatically. You start a managed transaction by passing a callback to sequelize.transaction. This callback can be async (and usually is).

The following will happen in this case:

  • Sequelize will automatically start a transaction and obtain a transaction object t
  • Then, Sequelize will execute the callback you provided, passing t into it
  • If your callback throws, Sequelize will automatically rollback the transaction
  • If your callback succeeds, Sequelize will automatically commit the transaction
  • Only then the sequelize.transaction call will settle:
    • Either resolving with the resolution of your callback
    • Or, if your callback throws, rejecting with the thrown error

Example code:

  1. try {
  2. const result = await sequelize.transaction(async (t) => {
  3. const user = await User.create({
  4. firstName: 'Abraham',
  5. lastName: 'Lincoln'
  6. }, { transaction: t });
  7. await user.setShooter({
  8. firstName: 'John',
  9. lastName: 'Boothe'
  10. }, { transaction: t });
  11. return user;
  12. });
  13. // If the execution reaches this line, the transaction has been committed successfully
  14. // `result` is whatever was returned from the transaction callback (the `user`, in this case)
  15. } catch (error) {
  16. // If the execution reaches this line, an error occurred.
  17. // The transaction has already been rolled back automatically by Sequelize!
  18. }

Note that t.commit() and t.rollback() were not called directly (which is correct).

Throw errors to rollback

When using the managed transaction you should never commit or rollback the transaction manually. If all queries are successful (in the sense of not throwing any error), but you still want to rollback the transaction, you should throw an error yourself:

  1. await sequelize.transaction(t => {
  2. const user = await User.create({
  3. firstName: 'Abraham',
  4. lastName: 'Lincoln'
  5. }, { transaction: t });
  6. // Woops, the query was successful but we still want to roll back!
  7. // We throw an error manually, so that Sequelize handles everything automatically.
  8. throw new Error();
  9. });

Automatically pass transactions to all queries

In the examples above, the transaction is still manually passed, by passing { transaction: t } as the second argument. To automatically pass the transaction to all queries you must install the cls-hooked (CLS) module and instantiate a namespace in your own code:

  1. const cls = require('cls-hooked');
  2. const namespace = cls.createNamespace('my-very-own-namespace');

To enable CLS you must tell sequelize which namespace to use by using a static method of the sequelize constructor:

  1. const Sequelize = require('sequelize');
  2. Sequelize.useCLS(namespace);
  3. new Sequelize(....);

Notice, that the useCLS() method is on the constructor, not on an instance of sequelize. This means that all instances will share the same namespace, and that CLS is all-or-nothing - you cannot enable it only for some instances.

CLS works like a thread-local storage for callbacks. What this means in practice is that different callback chains can access local variables by using the CLS namespace. When CLS is enabled sequelize will set the transaction property on the namespace when a new transaction is created. Since variables set within a callback chain are private to that chain several concurrent transactions can exist at the same time:

  1. sequelize.transaction((t1) => {
  2. namespace.get('transaction') === t1; // true
  3. });
  4. sequelize.transaction((t2) => {
  5. namespace.get('transaction') === t2; // true
  6. });

In most case you won’t need to access namespace.get('transaction') directly, since all queries will automatically look for a transaction on the namespace:

  1. sequelize.transaction((t1) => {
  2. // With CLS enabled, the user will be created inside the transaction
  3. return User.create({ name: 'Alice' });
  4. });

Concurrent/Partial transactions

You can have concurrent transactions within a sequence of queries or have some of them excluded from any transactions. Use the transaction option to control which transaction a query belongs to:

Note: SQLite does not support more than one transaction at the same time.

With CLS enabled

  1. sequelize.transaction((t1) => {
  2. return sequelize.transaction((t2) => {
  3. // With CLS enabled, queries here will by default use t2.
  4. // Pass in the `transaction` option to define/alter the transaction they belong to.
  5. return Promise.all([
  6. User.create({ name: 'Bob' }, { transaction: null }),
  7. User.create({ name: 'Mallory' }, { transaction: t1 }),
  8. User.create({ name: 'John' }) // this would default to t2
  9. ]);
  10. });
  11. });

Passing options

The sequelize.transaction method accepts options.

For unmanaged transactions, just use sequelize.transaction(options).

For managed transactions, use sequelize.transaction(options, callback).

Isolation levels

The possible isolations levels to use when starting a transaction:

  1. const { Transaction } = require('sequelize');
  2. // The following are valid isolation levels:
  3. Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED // "READ UNCOMMITTED"
  4. Transaction.ISOLATION_LEVELS.READ_COMMITTED // "READ COMMITTED"
  5. Transaction.ISOLATION_LEVELS.REPEATABLE_READ // "REPEATABLE READ"
  6. Transaction.ISOLATION_LEVELS.SERIALIZABLE // "SERIALIZABLE"

By default, sequelize uses the isolation level of the database. If you want to use a different isolation level, pass in the desired level as the first argument:

  1. const { Transaction } = require('sequelize');
  2. await sequelize.transaction({
  3. isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE
  4. }, async (t) => {
  5. // Your code
  6. });

You can also overwrite the isolationLevel setting globally with an option in the Sequelize constructor:

  1. const { Sequelize, Transaction } = require('sequelize');
  2. const sequelize = new Sequelize('sqlite::memory:', {
  3. isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE
  4. });

Note for MSSQL: The SET ISOLATION LEVEL queries are not logged since the specified isolationLevel is passed directly to tedious.

Usage with other sequelize methods

The transaction option goes with most other options, which are usually the first argument of a method.

For methods that take values, like .create, .update(), etc. transaction should be passed to the option in the second argument.

If unsure, refer to the API documentation for the method you are using to be sure of the signature.

Examples:

  1. await User.create({ name: 'Foo Bar' }, { transaction: t });
  2. await User.findAll({
  3. where: {
  4. name: 'Foo Bar'
  5. },
  6. transaction: t
  7. });

The afterCommit hook

A transaction object allows tracking if and when it is committed.

An afterCommit hook can be added to both managed and unmanaged transaction objects:

  1. // Managed transaction:
  2. await sequelize.transaction(async (t) => {
  3. t.afterCommit(() => {
  4. // Your logic
  5. });
  6. });
  7. // Unmanaged transaction:
  8. const t = await sequelize.transaction();
  9. t.afterCommit(() => {
  10. // Your logic
  11. });
  12. await t.commit();

The callback passed to afterCommit can be async. In this case:

  • For a managed transaction: the sequelize.transaction call will wait for it before settling;
  • For an unmanaged transaction: the t.commit call will wait for it before settling.

Notes:

  • The afterCommit hook is not raised if the transaction is rolled back;
  • The afterCommit hook does not modify the return value of the transaction (unlike most hooks)

You can use the afterCommit hook in conjunction with model hooks to know when a instance is saved and available outside of a transaction

  1. User.afterSave((instance, options) => {
  2. if (options.transaction) {
  3. // Save done within a transaction, wait until transaction is committed to
  4. // notify listeners the instance has been saved
  5. options.transaction.afterCommit(() => /* Notify */)
  6. return;
  7. }
  8. // Save done outside a transaction, safe for callers to fetch the updated model
  9. // Notify
  10. });

Locks

Queries within a transaction can be performed with locks:

  1. return User.findAll({
  2. limit: 1,
  3. lock: true,
  4. transaction: t1
  5. });

Queries within a transaction can skip locked rows:

  1. return User.findAll({
  2. limit: 1,
  3. lock: true,
  4. skipLocked: true,
  5. transaction: t2
  6. });