トランザクション
1つ以上のドキュメントに対して複数のミューテーション(mutation)をアトミックに実行し、スケール時のパフォーマンスを向上させます。
トランザクションを使う理由
複数のドキュメントを、すべての変更が成功するか、あるいは何も反映されないかの「単一のアトミック操作」として更新する必要がある場合があります。トランザクションがないと、一連の更新の途中で失敗が起きた際にデータが不整合な状態で残ってしまう可能性があります。たとえば、2つの口座間で残高を移動するには、引き落としと入金が必ずセットで完了する必要があります。
概要
Squid でトランザクションを実行するには、squid オブジェクトが提供する runInTransaction メソッドを使用します。このメソッドは callback 関数をパラメータとして受け取り、その関数はトランザクションのコンテキスト内で実行されます。
await squid.runInTransaction(async (transactionId: string) => {
const user1 = squid.collection<User>('users').doc('user_1_id');
const user2 = squid.collection<User>('users').doc('user_2_id');
await user1.update({ name: 'Alice' }, transactionId);
await user2.update({ name: 'Bob' }, transactionId);
});
トランザクション内で変更を適用する場合、その変更はすぐには反映されませんが、callback 関数が完了した時点でオプティミスティック(optimistically)に反映されます。
これは、トランザクションなしでミューテーションを適用する場合(変更が即時かつオプティミスティックに反映される)とは対照的です。トランザクションの一部として適用されるミューテーションは、いずれも即座に resolve します。変更がサーバー側で適用されたことを確実にするには、runInTransaction が返す promise が resolve するのを待つ必要があります。
コアコンセプト
トランザクションとクエリ
Squid はトランザクション内からのクエリをサポートしていません。たとえば、次のコードはトランザクション内でデッドロックを引き起こします。
const user1 = squid.collection<User>('users').doc('user_1_id');
await squid.runInTransaction(async (transactionId: string) => {
// This query inside the transaction will cause a deadlock
const user1Data = await user1.snapshot();
await user1.update({ loginCount: user1Data.loginCount + 1 });
});
代わりに、トランザクション開始前に必要なデータを取得してください。
const user1 = squid.collection<User>('users').doc('user_1_id');
const user1Data = await user1.snapshot();
await squid.runInTransaction(async (transactionId: string) => {
if (user1Data) {
await user1.update({ loginCount: user1Data.loginCount + 1 }, transactionId);
}
});
クロスコネクタ(cross-connector)トランザクション
複数の connector(コネクタ)にまたがってトランザクションを適用する場合、各 connector はそれぞれアトミックに更新されます。ただし、一方の connector は更新に成功したが、もう一方は失敗する可能性があります。connector をまたいだアトミック更新(グローバルなアトミック性)はサポートされていません。
クロスコネクタトランザクションが提供するのは「connector ごとのアトミック性」であり、「グローバルなアトミック性」ではありません。これに合わせてデータモデルを設計してください。
エラーハンドリング
| エラー | 原因 | 解決策 |
|---|---|---|
| デッドロック | トランザクションの callback 内でクエリを実行している | runInTransaction を呼ぶ前に必要なデータをすべて取得する |
| トランザクションのタイムアウト | トランザクションの callback の完了に時間がかかりすぎた | トランザクションを小さく・速く保つ。重い計算は callback の外に移す |
| 部分的な失敗(クロスコネクタ) | 一方の connector は成功したが、もう一方が失敗した | connector ごとのアトミック性を前提に設計する。重要なアトミック操作は単一 connector の利用を検討する |
| セキュリティルールによる拒否 | トランザクション内のミューテーションが security rules によりブロックされた | 対象となるすべての collection に対してユーザーが書き込み権限を持つことを確認する |
ベストプラクティス
-
デッドロックを避けるため、トランザクションの前にデータを取得します。
Client code// Fetch first
const accountA = await squid.collection<Account>('accounts').doc('a').snapshot();
const accountB = await squid.collection<Account>('accounts').doc('b').snapshot();
// Then transact
if (accountA && accountB) {
await squid.runInTransaction(async (txId: string) => {
await squid
.collection<Account>('accounts')
.doc('a')
.update({ balance: accountA.balance - 100 }, txId);
await squid
.collection<Account>('accounts')
.doc('b')
.update({ balance: accountB.balance + 100 }, txId);
});
} -
トランザクションを小さく保つため、アトミックである必要があるミューテーションだけを含めます。計算やバリデーションは callback の外へ移します。
-
callback 内のすべてのミューテーションに
transactionIdを渡します。渡し忘れると、そのミューテーションはトランザクションの外で実行されます。 -
確実なオールオアナッシング(all-or-nothing)のアトミック性が必要な場合は、クロスコネクタトランザクションを避けます。重要なアトミック操作には単一の connector を使用してください。