Node.js
Firebase
Firestore

Cloud Firestore で複数の DocumentReference に対し Transaction を実行する

Firestore では、 トランザクションと一括書き込み  |  Firebase にあるように、下記のコードで transaction が実行できます。

var transaction = db.runTransaction(t => {
    return t.get(cityRef)
        .then(doc => {
            // Add one person to the city population
            var newPopulation = doc.data().population + 1;
            t.update(cityRef, { population: newPopulation });
        });
})
.then(result => {
    console.log('Transaction success!');
})
.catch(err => {
    console.log('Transaction failure:', err);
});

この例だと transaction は1つの DocumentReference に対してのみ行われていますが、複数の reference に対しても Transaction を張ってみます。

複数 Transaction

3つの商品があり、同時に1つずつ在庫数を減らしているサンプルコードです。
※ TypeScript です。

const decreaseStock = async (productRefs: FirebaseFirestore.DocumentReference[]) => {
  return admin.firestore().runTransaction(async (transaction) => {
    const promises: Promise<any>[] = []
    for (const product of productRefs) {
      const t = transaction.get(product).then(tproduct => {
        const newStock = tproduct.data().stock - 1
        transaction.update(product, { stock: newStock })
      })
      promises.push(t)
    }
    return Promise.all(promises)
  })
}

const productData = [...Array(3).keys()].map(index => {
  return { price: 5000, stock: 1000, index: index }
})
const productRefs = await Promise.all(productData.map(data => {
  return admin.firestore().collection('product').add(data)
}))

await decreaseStock(productRefs)

for の中で transaction.get を複数作成して、 Promise.all でそれらを実行するようにしています。

これで3つの商品の更新に対し Transaction を実行できました。3つの商品全て stock: 999 になっており、問題なさそうです。

本当に Transaction になっているのか検証する

本当に Transaction として機能しているのか、3つの商品のうち1つの更新が失敗する状況を作ってみましょう。
Transaction として実行しているので、1つの商品の更新がこけたら他の2つの商品の更新もされないはずです。

3つある商品のうち、 index === 2 だったら throw しています。

const decreaseStock = async (productRefs: FirebaseFirestore.DocumentReference[]) => {
  return admin.firestore().runTransaction(async (transaction) => {
    const promises: Promise<any>[] = []
    for (const product of productRefs) {
      const t = transaction.get(product).then(tproduct => {
          if (tproduct.data().index === 2) {
            throw `index 2`
          } else {
            const newStock = tproduct.data().stock - 1
            transaction.update(product, { stock: newStock })
          }
      })
      promises.push(t)
    }
    return Promise.all(promises)
  })
}

const productData = [...Array(3).keys()].map(index => {
  return { price: 5000, stock: 1000, index: index }
})
const productRefs = await Promise.all(productData.map(data => {
  return admin.firestore().collection('product').add(data)
}))

await decreaseStock(productRefs)

この結果は商品3つとも stock: 1000 になっていて、ちゃんと Transaction になっています。
Transaction 内部のすべてが成功しないと update されません。

おわり

ドキュメントには1つの DocumentReference に対してしかのサンプルがありませんが、複数の DocumentReference に対しても Transaction ができました。
複数に対し同時に Transaction を張るということはロック時間が長くなってしまうので、積極的に使うのは避けたいですね。