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 内で実行します。
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 を引き起こします。
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 を取得します。
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 | 原因 | 解決策 |
|---|---|---|
| Deadlock | transaction callback 内で query を実行した | runInTransaction を呼び出す前に、必要なすべての data を取得する |
| Transaction timeout | transaction callback の完了に時間がかかりすぎた | transaction は小さく高速に保ち、heavy computation は callback の外に移動する |
| Partial failure(cross-connector) | 1 つの connector は成功したが、別の connector は失敗した | connector ごとの atomicity を考慮して設計し、critical な atomic operation には単一 connector の使用を検討する |
| Security rule rejection | transaction 内の mutation が security rules により block された | 関係するすべての collection に対する write permission を user が持つことを確認する |
ベストプラクティス
-
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);
});
} -
atomic でなければならない mutation のみを含めて、transaction を小さく保ちます。computation と validation は callback の外に移動します。
-
callback 内のすべての mutation に
transactionIdを渡します。渡し忘れると、mutation は transaction 外で実行されます。 -
確実な all-or-nothing atomicity が必要な場合は、cross-connector transaction を避けます。critical な atomic operation には単一 connector を使用してください。