跳至主要內容
版本:v6 - stable

交易

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 注意事項: 由於指定的 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,
});