トランザクション
Sequelizeはデフォルトではトランザクションを使用しません。ただし、Sequelizeを本番環境で使用する場合は、必ずトランザクションを使用するようにSequelizeを設定する必要があります。
Sequelizeは、トランザクションを使用する2つの方法をサポートしています。
-
非管理型トランザクション: トランザクションのコミットとロールバックは、ユーザーが手動で行う必要があります(適切なSequelizeメソッドを呼び出すことによって)。
-
管理型トランザクション: Sequelizeは、エラーがスローされた場合はトランザクションを自動的にロールバックし、それ以外の場合はトランザクションをコミットします。また、CLS(継続ローカルストレージ)が有効になっている場合、トランザクションコールバック内のすべてのクエリは、自動的にトランザクションオブジェクトを受け取ります。
非管理型トランザクション
まず例から始めましょう
// 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();
});
すべてのクエリにトランザクションを自動的に渡す
上記の例では、トランザクションは、2番目の引数として{ 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
は2番目の引数のオプションに渡す必要があります。
不明な場合は、使用しているメソッドの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
});
ロック
transaction
内のクエリはロックを使用して実行できます。
return User.findAll({
limit: 1,
lock: true,
transaction: t1,
});
トランザクション内のクエリは、ロックされた行をスキップできます。
return User.findAll({
limit: 1,
lock: true,
skipLocked: true,
transaction: t2,
});