事务
默认情况下,Sequelize 不使用事务. 但是,对于 Sequelize 的生产环 境使用,你绝对应该将 Sequelize 配置为使用事务.
Sequelize 支持两种使用事务的方式:
-
非托管事务: 提交和回滚事务应由用户手动完成(通过调用适当的 Sequelize 方法).
-
托管事务: 如果引发任何错误,Sequelize 将自动回滚事务,否则将提交事务. 另外,如果启用了CLS(连续本地存储),则事务回调中的所有查询将自动接收事务对象.
非托管事务
让我们从一个例子开始:
// 首先, 我们从你的连接开始一个事务并将其保存到一个变量中
const t = await sequelize.transaction();
try {
// 然后,我们进行一些调用以将此事务作为参数传递:
const user = await User.create({
firstName: 'Bart',
lastName: 'Simpson'
}, { transaction: t });
await user.addSibling({
firstName: 'Lisa',
lastName: 'Simpson'
}, { transaction: t });
// 如果执行到此行,且没有引发任何错误.
// 我们提交事务.
await t.commit();
} catch (error) {
// 如果执行到达此行,则抛出错误.
// 我们回滚事务.
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;
});
// 如果执行到此行,则表示事务已成功提交,`result`是事务返回的结果
// `result` 就是从事务回调中返回的结果(在这种情况下为 `user`)
} catch (error) {
// 如果执行到此,则发生错误.
// 该事务已由 Sequelize 自动回滚!
}
注意,t.commit()
和 t.rollback()
没有被直接调用.
抛出错误以回滚
使用托管事务时,你 不应 手动提交或回滚事务. 如果所有查询都成功(就不引发任何错误而言),但是你仍然想回滚事务,那么你应该自己引发一个错误:
await sequelize.transaction(async t => {
const user = await User.create({
firstName: 'Abraham',
lastName: 'Lincoln'
}, { transaction: t });
// 查询成功,但我们仍要回滚!
// 我们手动引发错误,以便 Sequelize 自动处理所有内容.
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) => {
// 启用 CLS 后,将在事务内部创建用户
return User.create({ name: 'Alice' });
});