交易
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 注意事項: 由於指定的 isolationLevel
會直接傳遞給 tedious
,因此不會記錄 SET ISOLATION LEVEL
查詢。
與其他 sequelize 方法一起使用
transaction
選項與大多數其他選項一起使用,這些選項通常是方法的第一個引數。
對於採用值的方法,例如 .create
、.update()
等,transaction
應傳遞至第二個引數中的選項。
如果不確定,請參閱您正在使用的方法的 API 文件,以確保簽名正確。
範例
await User.create({ name: 'Foo Bar' }, { transaction: t });
await User.findAll({
where: {
name: 'Foo Bar',
},
transaction: t,
});
afterCommit
hook
transaction
物件允許追蹤是否以及何時提交。
可以將 afterCommit
hook 新增至託管和非託管交易物件
// 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
hook; afterCommit
hook 不會修改交易的傳回值(與大多數 hook 不同)
您可以將 afterCommit
hook 與模型 hook 結合使用,以了解實例何時儲存且在交易外部可用
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
});
鎖定
transaction
中的查詢可以使用鎖定來執行
return User.findAll({
limit: 1,
lock: true,
transaction: t1,
});
交易中的查詢可以跳過鎖定的列
return User.findAll({
limit: 1,
lock: true,
skipLocked: true,
transaction: t2,
});