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

Transactions

scale 時の performance 向上のため、1 つ以上の document に対する複数 mutation を atomic に実行します。​

Transactions を使用する理由​

複数の document を、すべての変更が成功するか、または一切成功しない単一の atomic operation として update する必要があります。transaction がない場合、一連の update の途中で failure が発生すると、data が不整合な state になる可能性があります。たとえば、2 つの account 間で balance を transfer するには、debit と credit の両方が一緒に完了する必要があります。

概要​

Squid で transaction を実行するには、squid object が提供する runInTransaction method を使用します。この method は parameter として callback function を受け取り、transaction の context 内で実行します。

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

transaction 内で変更を適用する場合、変更は即座には反映されませんが、callback function が完了すると optimistic に反映されます。

これは、transaction なしで mutation を適用する場合とは対照的です。transaction なしでは変更は即座に optimistic に適用されます。transaction の一部として適用される mutation は、即座に resolve します。変更が server に適用されることを確認するには、runInTransaction から返される promise が resolve するまで待つ必要があります。

コアコンセプト​

Transactions と Query​

Squid は transaction 内からの query をサポートしません。たとえば、以下の code は transaction 内で deadlock を引き起こします。

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

代わりに、transaction を開始する前に必要な data を取得します。

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 transaction​

複数 connector に対して transaction を適用する場合、各 connector は atomic に update されます。ただし、1 つの connector では update が成功し、別の connector では failure する可能性があります。cross-connector の atomic update はサポートされていません。

注記

Cross-connector transaction は connector ごとの atomicity を提供しますが、global atomicity は提供しません。それに応じて data model を設計してください。

Error Handling​

Error原因解決策
Deadlocktransaction callback 内で query を実行したrunInTransaction を呼び出す前に、必要なすべての data を取得する
Transaction timeouttransaction callback の完了に時間がかかりすぎたtransaction は小さく高速に保ち、heavy computation は callback の外に移動する
Partial failure(cross-connector)1 つの connector は成功したが、別の connector は失敗したconnector ごとの atomicity を考慮して設計し、critical な atomic operation には単一 connector の使用を検討する
Security rule rejectiontransaction 内の mutation が security rules により block された関係するすべての collection に対する write permission を user が持つことを確認する

ベストプラクティス​

  1. deadlock を回避するため、transaction の前に data を取得します。

    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. atomic でなければならない mutation のみを含めて、transaction を小さく保ちます。computation と validation は callback の外に移動します。

  3. callback 内のすべての mutation に transactionId を渡します。渡し忘れると、mutation は transaction 外で実行されます。

  4. 確実な all-or-nothing atomicity が必要な場合は、cross-connector transaction を避けます。critical な atomic operation には単一 connector を使用してください。