メインコンテンツまでスキップ

トランザクション

1つ以上のドキュメントに対して複数のミューテーション(mutation)をアトミックに実行し、スケール時のパフォーマンスを向上させます。

トランザクションを使う理由

複数のドキュメントを、すべての変更が成功するか、あるいは何も反映されないかの「単一のアトミック操作」として更新する必要がある場合があります。トランザクションがないと、一連の更新の途中で失敗が起きた際にデータが不整合な状態で残ってしまう可能性があります。たとえば、2つの口座間で残高を移動するには、引き落としと入金が必ずセットで完了する必要があります。

概要

Squid でトランザクションを実行するには、squid オブジェクトが提供する runInTransaction メソッドを使用します。このメソッドは callback 関数をパラメータとして受け取り、その関数はトランザクションのコンテキスト内で実行されます。

Client code
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 はトランザクション内からのクエリをサポートしていません。たとえば、次のコードはトランザクション内でデッドロックを引き起こします。

Client code
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 });
});

代わりに、トランザクション開始前に必要なデータを取得してください。

Client code
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 に対してユーザーが書き込み権限を持つことを確認する

ベストプラクティス

  1. デッドロックを避けるため、トランザクションの前にデータを取得します。

    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);
    });
    }
  2. トランザクションを小さく保つため、アトミックである必要があるミューテーションだけを含めます。計算やバリデーションは callback の外へ移します。

  3. callback 内のすべてのミューテーションに transactionId を渡します。渡し忘れると、そのミューテーションはトランザクションの外で実行されます。

  4. 確実なオールオアナッシング(all-or-nothing)のアトミック性が必要な場合は、クロスコネクタトランザクションを避けます。重要なアトミック操作には単一の connector を使用してください。