初めに
Firestoreを利用する上で当たり前のように行うドキュメントの追加ですが、ちょっと間違うと大変なことになりえるので書いておきたいと思います。
2つの追加方法
公式ページにちゃんと書いてありますが、ドキュメントの追加には2つのやり方が存在しています。
一つ目は、addDoc
(v8だとcollection("コレクション名").add
)を使う方法です。
関数もその名の通りです。
import { getFirestore, collection, addDoc } from "firebase/firestore";
await addDoc(collection(getFirestore(), "cities"), {name: '東京'});
await db.collection("cities").add({name: '東京'});
もう一つは、setDocを使う方法です。
import { getFirestore, collection, doc, setDoc } from "firebase/firestore";
await setDoc(doc(collection(getFirestore(), "cities"))), {name: '東京'});
await db.collection("cities").doc().set({name: '東京'});
公式ページではこれは等価であるという説明があり、用途により使い分ければいいという話になっています。
実はここに罠があります。
setDocはあくまで上書きするもの
setDoc
なのですが、idとしての引数をつけないことでdocで自動採番して追加する仕組みをとっています。
なので、docが生成するidによっては上書きされることになります!
docで自動生成するものは、被らないんじゃないの?と思ってらっしゃる方がいるかもしれません。
が、実はここでの自動生成は単純にランダムな文字列を作っているだけなので、かなり可能性は低いにせよ同じidが発行される可能性が残ります。
addDocは上書きを許さない
じゃあaddDoc
も同じく内部的にidを自動採番してるので同じリスクをもっているんじゃない?と思う方もいらっしゃると思います。
公式ページで同じであると書いてあるくらいですからね。
ですが実際には差がありました。
ちょっと前に書いた以下の記事でそれに気づきました。
ここのaddDoc
をしたときにcurrentDocument.exists
がfalse
になっていました。
実際にfirebase-js-sdk
のソースコードを見てみると、確かにsetDoc
はドキュメントがあるかないかのチェックはしておらず、addDoc
はレコードがないことをチェックしているようでした。
なので、確実にドキュメントを上書きせず追加をするにはaddDoc
を使えばいいということになります!
以上!
としたいところですが、たまーに先にidを発行して追加したいってときがありますよね?
では、どう解決するか?
ここでセキュリティルールの登場です。
セキュリティルールで追加 or 変更の条件を絞る
いくつかパターンがあると思うので、単純な順から説明していきます。
更新をさせない
これが一番シンプルかと思います。
セキュリティルール的には以下の書き方になると思います。
match /cities/{cityId} {
allow create: if true;
}
セキュリティルールはホワイトリストで書いていくので、updateを抜いて書けばいいです。
実際にはこれだと弱すぎるのでtrue
のところが何らかの条件になると思います。
ただ、これだと更新処理ができなくなるのでなかなか使えるケースは少ないと思います。
更新できるケースを制限する
更新と追加の処理が明確に分けられるようにして、それに対してセキュリティルールを書く方法があります。
元々のデータ構造に依存しないようにするために、ここでは追加日時を持たせることで、更新させることをガードする方法を記載します。
まず追加の時、追加した時間を入れるようにします。
import { getFirestore, collection, addDoc, serverTimestamp } from "firebase/firestore";
await addDoc(
collection(getFirestore(), "cities"),
{ name: '東京', createdAt: serverTimestamp() }
);
意図的な更新の時はこのcreatedAt
が更新されないことをルールで表現すればいいので、以下の形になるかと思います。
match /cities/{cityId} {
allow create: if true;
allow update: if request.resource.data.createdAt == resource.data.createdAt;
}
追加のときにわざとnull
を入れたりすると避けられるので、もっと厳しく書くと、以下のように書けます。
match /cities/{cityId} {
allow create: if request.time == request.resource.data.createdAt;
allow update: if request.resource.data.createdAt == resource.data.createdAt;
}
firebase-adminを使う場合
firebase-admin
のSDKを使うとFirestoreのセキュリティルールは効果がないので上の方法は使えません。
使えない代わりに、firebase-admin
では、DocumentRefrence
の中にcreate
という関数が存在していて、それを使えばドキュメントIDが被ったときにエラーが出るようになります。
Batchにも実装されているので、Cloud Functions
などで、idを先に作るようなときはset
ではなくcreate
を使うようにすればよさそうです。
終わりに
一番単純な追加という操作ですが、意外とハマりどころがあるものですね。
実際にはidが被るような事態は、ずっと大量データを作っていくようなコレクションでなければなかなか起きないと思います。
とはいえ、発生するとめんどうなことになる可能性はあるので、やれることはやっておいた方がいいと思います。
そもそもでいうとsetDoc
にオプションで追加のみの指定を作ってくれればセキュリティルールでわざわざこんなことする必要はないんですが、いつか対応されますかね。
Firebase SDKのGithubのissueに載ってたのでいつか対応されるかもしれませんね。