トランザクション
スケール時のパフォーマンスを向上させるために、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 関数が完了すると楽観的(optimistic)に反映されます。
これは、トランザクションなしで mutation を適用する場合(変更が即時かつ楽観的に反映される)とは対照的です。トランザクションの一部として適用される mutation はすぐに resolve します。変更がサーバーに適用されたことを確実にするには、runInTransaction が返す promise が resolve するのを待つ必要があります。
コアコンセプト
トランザクションと query
Squid はトランザクション内からの query をサポートしていません。たとえば、次のコードはトランザクション内でデッドロックを引き起こします。
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 transactions
複数の connector に対してトランザクションを適用する場合、それぞれの connector はアトミックに更新されます。しかし、一方の connector は更新に成功した一方で、もう一方が失敗する可能性があります。connector をまたいだアトミック更新はサポートされていません。
Cross-connector transactions は connector ごとのアトミック性を提供しますが、グローバルなアトミック性は提供しません。それに合わせてデータモデルを設計してください。
エラーハンドリング
| エラー | 原因 | 解決策 |
|---|---|---|
| Deadlock | トランザクション callback 内で query を実行している | runInTransaction を呼び出す前に必要なデータをすべて取得する |
| Transaction timeout | トランザクション callback の完了に時間がかかりすぎた | トランザクションを小さく高速に保つ。重い計算は callback の外へ移動する |
| Partial failure (cross-connector) | ある connector は成功したが、別の connector が失敗した | connector ごとのアトミック性を前提に設計する。重要なアトミック操作には単一 connector の使用を検討する |
| Security rule rejection | トランザクション内の mutation が 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);
});
} -
必ずアトミックである必要がある mutation のみを含めて、トランザクションを小さく保ちます。計算やバリデーションは callback の外へ移動してください。
-
callback 内のすべての mutation に
transactionIdを渡します。渡し忘れると、その mutation はトランザクション外で実行されます。 -
all-or-nothing のアトミック性が保証される必要がある場合は、cross-connector transactions を避けます。重要なアトミック操作には単一 connector を使用してください。