事务
Sequelize 默认情况下不使用事务。但是,对于 Sequelize 的生产就绪使用,您绝对应该配置 Sequelize 以使用事务。
Sequelize 支持两种使用事务的方式
-
非托管事务:事务的提交和回滚应由用户手动完成(通过调用相应的 Sequelize 方法)。
-
托管事务:如果抛出任何错误,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,
});