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 を張るということはロック時間が長くなってしまうので、積極的に使うのは避けたいですね。