跳至主要内容
版本:v6 - 稳定版

事务

Sequelize 默认情况下不使用事务。但是,对于 Sequelize 的生产就绪使用,您绝对应该配置 Sequelize 以使用事务。

Sequelize 支持两种使用事务的方式

  1. 非托管事务:事务的提交和回滚应由用户手动完成(通过调用相应的 Sequelize 方法)。

  2. 托管事务:如果抛出任何错误,Sequelize 将自动回滚事务,否则提交事务。此外,如果启用了 CLS(Continuation Local Storage),则事务回调内的所有查询将自动接收事务对象。

非托管事务

让我们从一个例子开始

// First, we start a transaction from your connection and save it into a variable
const t = await sequelize.transaction();

try {
// Then, we do some calls passing this transaction as an option:

const user = await User.create(
{
firstName: 'Bart',
lastName: 'Simpson',
},
{ transaction: t },
);

await user.addSibling(
{
firstName: 'Lisa',
lastName: 'Simpson',
},
{ transaction: t },
);

// If the execution reaches this line, no errors were thrown.
// We commit the transaction.
await t.commit();
} catch (error) {
// If the execution reaches this line, an error was thrown.
// We rollback the transaction.
await t.rollback();
}

如上所示,非托管事务方法要求您在必要时手动提交和回滚事务。

托管事务

托管事务会自动处理事务的提交或回滚。您可以通过将回调传递给 sequelize.transaction 来启动托管事务。此回调可以是 async(通常是)。

在这种情况下会发生以下情况

  • Sequelize 将自动启动事务并获取事务对象 t
  • 然后,Sequelize 将执行您提供的回调,并将 t 传递给它
  • 如果您的回调抛出错误,Sequelize 将自动回滚事务
  • 如果您的回调成功,Sequelize 将自动提交事务
  • 只有在那之后,sequelize.transaction 调用才会结束
    • 要么使用回调的结果解析
    • 要么,如果您的回调抛出错误,则使用抛出的错误拒绝

示例代码

try {
const result = await sequelize.transaction(async t => {
const user = await User.create(
{
firstName: 'Abraham',
lastName: 'Lincoln',
},
{ transaction: t },
);

await user.setShooter(
{
firstName: 'John',
lastName: 'Boothe',
},
{ transaction: t },
);

return user;
});

// If the execution reaches this line, the transaction has been committed successfully
// `result` is whatever was returned from the transaction callback (the `user`, in this case)
} catch (error) {
// If the execution reaches this line, an error occurred.
// The transaction has already been rolled back automatically by Sequelize!
}

请注意,t.commit()t.rollback() 没有被直接调用(这是正确的)。

抛出错误以回滚

使用托管事务时,您永远不应该手动提交或回滚事务。如果所有查询都成功(在没有抛出任何错误的意义上),但您仍然希望回滚事务,则应自行抛出错误

await sequelize.transaction(async t => {
const user = await User.create(
{
firstName: 'Abraham',
lastName: 'Lincoln',
},
{ transaction: t },
);

// Woops, the query was successful but we still want to roll back!
// We throw an error manually, so that Sequelize handles everything automatically.
throw new Error();
});

自动将事务传递给所有查询

在上面的示例中,事务仍然是手动传递的,方法是将 { transaction: t } 作为第二个参数传递。要自动将事务传递给所有查询,您必须安装cls-hooked(CLS)模块并在您自己的代码中实例化一个命名空间

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

要启用 CLS,您必须使用 Sequelize 构造函数的静态方法告诉 Sequelize 使用哪个命名空间

const Sequelize = require('sequelize');
Sequelize.useCLS(namespace);

new Sequelize(....);

请注意,useCLS() 方法位于构造函数上,而不是 Sequelize 实例上。这意味着所有实例将共享相同的命名空间,并且 CLS 是全有或全无的 - 您不能只为某些实例启用它。

CLS 就像回调的线程本地存储。在实践中,这意味着不同的回调链可以通过使用 CLS 命名空间来访问局部变量。启用 CLS 时,Sequelize 会在创建新事务时在命名空间上设置 transaction 属性。由于回调链中设置的变量对该链是私有的,因此可以同时存在多个并发事务

sequelize.transaction(t1 => {
namespace.get('transaction') === t1; // true
});

sequelize.transaction(t2 => {
namespace.get('transaction') === t2; // true
});

在大多数情况下,您不需要直接访问 namespace.get('transaction'),因为所有查询都会自动在命名空间上查找事务

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

并发/部分事务

您可以在一系列查询中拥有并发事务,或者将其中一些查询排除在任何事务之外。使用 transaction 选项来控制查询属于哪个事务

注意:SQLite 同时不支持多个事务。

启用 CLS 后

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

传递选项

sequelize.transaction 方法接受选项。

对于非托管事务,只需使用 sequelize.transaction(options)

对于托管事务,使用 sequelize.transaction(options, callback)

隔离级别

启动事务时可用的隔离级别

const { Transaction } = require('sequelize');

// The following are valid isolation levels:
Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED; // "READ UNCOMMITTED"
Transaction.ISOLATION_LEVELS.READ_COMMITTED; // "READ COMMITTED"
Transaction.ISOLATION_LEVELS.REPEATABLE_READ; // "REPEATABLE READ"
Transaction.ISOLATION_LEVELS.SERIALIZABLE; // "SERIALIZABLE"

默认情况下,Sequelize 使用数据库的隔离级别。如果要使用不同的隔离级别,请将其作为第一个参数传递

const { Transaction } = require('sequelize');

await sequelize.transaction(
{
isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE,
},
async t => {
// Your code
},
);

您还可以使用 Sequelize 构造函数中的选项全局覆盖 isolationLevel 设置

const { Sequelize, Transaction } = require('sequelize');

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

MSSQL 注意:SET ISOLATION LEVEL 查询不会被记录,因为指定的 isolationLevel 会直接传递给 tedious

与其他 Sequelize 方法一起使用

transaction 选项与大多数其他选项一起使用,这些选项通常是方法的第一个参数。

对于采用值的函数,例如 .create.update() 等,应将 transaction 传递到第二个参数中的选项。

如果不确定,请参考您正在使用的函数的 API 文档以确保签名。

示例

await User.create({ name: 'Foo Bar' }, { transaction: t });

await User.findAll({
where: {
name: 'Foo Bar',
},
transaction: t,
});

afterCommit 钩子

transaction 对象允许跟踪它是否以及何时提交。

可以将 afterCommit 钩子添加到托管和非托管事务对象。

// Managed transaction:
await sequelize.transaction(async t => {
t.afterCommit(() => {
// Your logic
});
});

// Unmanaged transaction:
const t = await sequelize.transaction();
t.afterCommit(() => {
// Your logic
});
await t.commit();

传递给 afterCommit 的回调可以是 async。在这种情况下

  • 对于托管事务:sequelize.transaction 调用将在其结束前等待;
  • 对于非托管事务:t.commit 调用将在其结束前等待。

注意

  • 如果事务回滚,则不会引发 afterCommit 钩子;
  • afterCommit 钩子不会修改事务的返回值(与大多数钩子不同)

您可以将 afterCommit 钩子与模型钩子结合使用,以了解实例何时保存并在事务外部可用

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

事务中的查询可以与锁一起执行

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

事务中的查询可以跳过已锁定行

return User.findAll({
limit: 1,
lock: true,
skipLocked: true,
transaction: t2,
});