【python3+firestore】複雑なトランザクション処理を掛ける

公式のサンプルだと、一つのドキュメントに対するトランザクションしか出ていないのですが、実際に使う時って二つ三つのドキュメントの整合性がとれてなきゃいけなかったりするよね、というお話。

結論

from google.cloud import firestore

class fsapp():
   def __init__(self):
       self.db = firestore.Client()
       print(self.db)
   def getDocRef(self,DocumentPath):
       return self.db.document(DocumentPath)
   def getTransaction(self):
       return self.db.transaction()

def main():
  fs = fsapp()
  transaction = fs.getTransaction()
  update_in_transaction(transaction, fs)

@firestore.transactional
def update_in_transaction(transaction, fs):
  documentPath1 = "hogeCollection/fooDocument"
  document1_ref = fs.getDocRef(documentPath1)
  doc1_snapshot = document1_ref.get(transaction=transaction)

  documentPath2 = "hogeCollection/barDocument"
  document2_ref = fs.getDocRef(documentPath2)
  doc2_snapshot = document1_ref.get(transaction=transaction)

  val1 = doc1_snapshot.get("fieldName")
  val2 = doc1_snapshot.get("fieldName2")

  newVal1 = int(val1) + 1
  newVal2 = int(val2) + int(val1)

  transaction.update(document1_ref , {'fieldName': newVal1,})
  transaction.update(document2_ref , {'fieldName2': newVal2,})

今回のポイント

・トランザクション処理の中では、複数のドキュメントのスナップショットを作れる。
・もちろん、複数のドキュメントに対してtransaction.update()できる。
・但し、読み込み→書き込みの順番を間違えないこと。(一度transaction.update()したら、例え別のドキュメントに対してであっても、スナップショットの取得をしない) ※間違えると無限ループする

そもそもトランザクションって何かっていうと

公式ドキュメント読むと、「読み込み→書き込みの一連の流れ」みたいに書いてあるけど、それだけだと使いどころがどうもピンと来ない。

例えば、在庫を管理するアプリを作る時に、「今の在庫数を読み込む→今日の入荷数を足す→合計数を書き込み直す」という処理があるとする。

それだけなら簡単だけど、これを「入荷の時は、今の在庫数と入荷数を読み込む→入荷数に今日の入荷数を足して書き込む→在庫数に今日の入荷数を足して書き込む」&「出荷の時は、在庫数と出荷数を読み込む→今日の出荷数を足して書き込む→在庫数から今日の出荷数を引いて書き込む」という仕組みに変えたいとする。

で、入荷の時に万一「在庫数と入荷数を読み込む→今日の入荷数を足して書き込む→何かのトラブルで処理がコケる」という事が起こった場合、入荷数と在庫数の整合性がとれなくなってしまう。困った。

というわけで、「DBの情報を読み込む→計算・処理をする→DBに書き込み直す」という一連の処理が全て終わるまで本番書き込みは行わず、必要な処理の全てが終わったことを確認してから本番書き込みを行う。

この一連の処理をトランザクションというんだって。

ついでに、処理の途中でDBに変更があった場合、それを検知して最初から処理をやり直してくれるっぽいんだけど、その辺はまだよく分かっていない……

複数のドキュメントに対してトランザクション

公式ドキュメントだと、一つのドキュメントに対して読み書きをするサンプルしか出ていないが、実際に使う時ってあっちこっちのドキュメントを読んでこないといけなかったりするわけで、そんな場合は、トランザクション処理の中で複数のドキュメントのスナップショットを取得すればオッケー。
もちろん、複数のドキュメントの更新も掛けられる。

但し、「読み→書き→読み」の順で処理を行うと、自分で掛けた更新を自分で検知して永遠に「読み→書き→読もうとしたらDBに変更あったからやり直し→読み→書き→変更あった」を繰り返してしまう。
読み込む必要があるドキュメントは最初に全部読み込んで、書き込む必要があるドキュメントは最後にまとめて書き込むのが吉。

※firestoreの基本的な導入方法はこっちの記事で。

from google.cloud import firestore

class fsapp(): # こうしておくと便利
   def __init__(self):
       self.db = firestore.Client()
       print(self.db)
   def getDocRef(self,DocumentPath):
       return self.db.document(DocumentPath)
   def getTransaction(self):
       return self.db.transaction()

def main():
  fs = fsapp()
  # 「トランザクション」を取得(なんかこう……処理を保留しておくための箱みたいなイメージ)
  transaction = fs.getTransaction()
  # トランザクション処理本体を呼びだし
  update_in_transaction(transaction, fs)

# トランザクション処理本体
@firestore.transactional
def update_in_transaction(transaction, fs):  # @firestore.transactionalを付与した関数の第一引数は必ずtransactionオブジェクト。
  # ドキュメントのパスを用意
  documentPath1 = "hogeCollection/fooDocument"
  # ドキュメントへの参照を用意
  document1_ref = fs.getDocRef(documentPath1)
  # スナップショットを取得(≒ドキュメントを読み込み)
  doc1_snapshot = document1_ref.get(transaction=transaction)
  # もう一つのドキュメントに対しても同様にスナップショットを取得
  documentPath2 = "hogeCollection/barDocument"
  document2_ref = fs.getDocRef(documentPath2)
  doc2_snapshot = document1_ref.get(transaction=transaction)

  # スナップショットの取得はまとめて行う方が安全。

  # スナップショットに対して.get("フィールド名")で値を取得できる
  val1 = doc1_snapshot.get("fieldName")
  val2 = doc1_snapshot.get("fieldName2")

  # 新しいデータを作る
  newVal1 = int(val1) + 1
  newVal2 = int(val2) + int(val1) # 場合によりDBに文字列で入ってたりするので、明示的に型変換。

  # アップデート
  # アップデートもまとめて行う方が安全。
  # 一度transaction.update()を使ったら、その先でget系の命令を使ってはいけない。
  transaction.update(document1_ref , {'fieldName': newVal1,})
  transaction.update(document2_ref , {'fieldName2': newVal2,})

この記事が気に入ったらサポートをしてみませんか?