公式ガイドで教えてくれなかったFirestoreのtransaction処理で連番IDを振る方法


本記事の投稿動機

  • Firestoreのtransaction処理実装で公式ガイドには載っていないやり方での実装が必要だったため、知識整理として投稿
  • おそらく連番IDを振るという実装はFirestore的にも推奨をしていませんので、この実装を実運用する場合は自己責任でおねがいいたします

本記事の対象読者

  • Cloud Firestoreを使用したことがあり、Firestoreにデータ追加、更新、削除といったオペレーションの想像がつく方
  • アンチパターンかもだけどfirestoreで連番IDを扱いたい方
  • 素直にauto-incrementを使うのが吉と分かってはいるが、連番IDを他通信と競合せずに扱いたい方

※ 実装はNuxt.js×TypeScriptです。

Firestoreのtransaction処理とは

  • 1つ以上のドキュメントに対して読み書きを行う一連のオペレーション
  • 最大 500 のドキュメントに書き込みを行うことができる

公式ガイドでのtransaction

公式ガイドの例では、コレクションのドキュメントが分かっている状態で、データを取得し、一部を編集して再度更新するといったサンプルとなっています。

また、以下の注意が必要です。

  • 読み取りオペレーションは書き込みオペレーションの前に実行する必要があります。
  • トランザクションが読み取るドキュメントに対して同時編集が影響する場合は、トランザクションを呼び出す関数(トランザクション関数)が複数回実行されることがあります。
  • トランザクション関数はアプリケーションの状態を直接変更してはなりません。
  • クライアントがオフラインの場合、トランザクションは失敗します。

今回、実装したいこと

  • 特定のコレクション(ドキュメント名は不明)からcreatedByを降順にし、limitをかけた上でデータを取得。
  • そのデータのtodoId(number)に1を足して、連番ID(newTodoId)として新規データオブジェクトと共に追加

なので、

  • transaction処理内でデータ取得(.get)とデータ新規追加(.set)をする必要があります。

※通常であれば新規のデータ追加は.addを使用します。transactionの場合は.addが用意されておらず。代わりに.setを使用します。.setは既にデータが有れば更新。無ければ新規追加となりますので注意が必要です。

transaction内でやること

1. まず連番IDの元となるIDを取得する

transactionでは読み書きが必須となっており、まずは読みから実装します。
runTransactionメソッド内でtransactionによるオペレーションが使用できるようになります。
今回、ドキュメント名は分かっておらず、orderByで降順に並び替えた後に最新の1件のみを取得します。
newTodoRefでは空docを指定し、referenceを作ります。

export async function addTodo(userId: string, todo: Todo) {
  await Firestore.runTransaction(async (transaction) => {
    const todoRef = await Firestore.collection('todos')
    const newTodoRef = await todoRef.doc()

    await transaction.get(newTodoRef).then(async () => {
      // 最新のtodoを1件のみ取得
      const querySnapshot = await todoRef.orderBy("createdAt", "desc").limit(1).get()
    })
  })
}

2. 連番IDを計算する

今回の場合は、元のIDに1を足すだけです。
先に変数newTodoIdを用意し、Promise.all内でtodo.todoId + 1を代入します。

if(querySnapshot) {
  let newTodoId!: number

  await Promise.all(querySnapshot.docs.map(async (todoDoc) => {
    const todo = await todoDoc.data()

    // 新しいnewTodoIdを作る
    newTodoId = await todo.todoId + 1
  }))
}

3. Firestoreに書き込む

あとはデータを整形して書き込むだけです。
書き込みは.addではなく、.setを用いることに注意が必要です。
前述の通り、.setは既にデータが有れば更新。無ければ新規追加となります。

const todoData: TodoData = {
  userId: userId,
  todoId: newTodoId,
  title: todo.title,
  content: todo.content
}

// TODOを新規追加
await transaction.set(newTodoRef, todoData)

実際の全体コード

/firestore/todo.ts

export async function addTodo(userId: string, todo: Todo) {
  await Firestore.runTransaction(async (transaction) => {
    const todoRef = await Firestore.collection('todos')
    const newTodoRef = await todoRef.doc()

    await transaction.get(newTodoRef).then(async () => {
      // 最新のtodoを1件のみ取得
      const querySnapshot = await todoRef.orderBy("createdAt", "desc").limit(1).get()

      if(querySnapshot) {
        let newTodoId!: number

        await Promise.all(querySnapshot.docs.map(async (todoDoc) => {
          const todo = await todoDoc.data()

          // 新しいnewTodoIdを作る
          newTodoId = await todo.todoId + 1
        }))

        const todoData: TodoData = {
          userId: userId,
          todoId: newTodoId,
          title: todo.title,
          content: todo.content
        }

        // TODOを新規追加
        await transaction.set(newTodoRef, todoData)
      }
    })
  })
}

最後に

transactionに一度慣れる、Firestoreを用いたのアプリ実装の幅がぐっと広がる気がします。

私も個人開発が趣味なのでどんどん取り入れていきたいです。

また、Firestoreを使用しておりませんが、個人開発で「thanks-mentions」というPWAをリリースしました!

Qiitaの記事をメンション付きで共有できるPWAとなっております。

Qiitaの記事を共有する際は「thanks-mentions」をよろしくお願いいたします!