8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Firestoreのドキュメント追加で気をつけたいこと

Last updated at Posted at 2022-11-30

初めに

Firestoreを利用する上で当たり前のように行うドキュメントの追加ですが、ちょっと間違うと大変なことになりえるので書いておきたいと思います。

2つの追加方法

公式ページにちゃんと書いてありますが、ドキュメントの追加には2つのやり方が存在しています。

一つ目は、addDoc(v8だとcollection("コレクション名").add)を使う方法です。
関数もその名の通りです。

v9の場合
import { getFirestore, collection, addDoc } from "firebase/firestore"; 

await addDoc(collection(getFirestore(), "cities"), {name: '東京'});
v8の場合
await db.collection("cities").add({name: '東京'});

もう一つは、setDocを使う方法です。

v9の場合
import { getFirestore, collection, doc, setDoc } from "firebase/firestore"; 

await setDoc(doc(collection(getFirestore(), "cities"))), {name: '東京'});
v8の場合
await db.collection("cities").doc().set({name: '東京'});

公式ページではこれは等価であるという説明があり、用途により使い分ければいいという話になっています。
実はここに罠があります。

setDocはあくまで上書きするもの

setDocなのですが、idとしての引数をつけないことでdocで自動採番して追加する仕組みをとっています。
なので、docが生成するidによっては上書きされることになります!

docで自動生成するものは、被らないんじゃないの?と思ってらっしゃる方がいるかもしれません。
が、実はここでの自動生成は単純にランダムな文字列を作っているだけなので、かなり可能性は低いにせよ同じidが発行される可能性が残ります。

addDocは上書きを許さない

じゃあaddDocも同じく内部的にidを自動採番してるので同じリスクをもっているんじゃない?と思う方もいらっしゃると思います。
公式ページで同じであると書いてあるくらいですからね。

ですが実際には差がありました。

ちょっと前に書いた以下の記事でそれに気づきました。

ここのaddDocをしたときにcurrentDocument.existsfalseになっていました。
実際にfirebase-js-sdkのソースコードを見てみると、確かにsetDocはドキュメントがあるかないかのチェックはしておらず、addDocはレコードがないことをチェックしているようでした。

なので、確実にドキュメントを上書きせず追加をするにはaddDocを使えばいいということになります!
以上!
としたいところですが、たまーに先にidを発行して追加したいってときがありますよね?

では、どう解決するか?
ここでセキュリティルールの登場です。

セキュリティルールで追加 or 変更の条件を絞る

いくつかパターンがあると思うので、単純な順から説明していきます。

更新をさせない

これが一番シンプルかと思います。
セキュリティルール的には以下の書き方になると思います。

match /cities/{cityId} {
  allow create: if true;
}

セキュリティルールはホワイトリストで書いていくので、updateを抜いて書けばいいです。
実際にはこれだと弱すぎるのでtrueのところが何らかの条件になると思います。

ただ、これだと更新処理ができなくなるのでなかなか使えるケースは少ないと思います。

更新できるケースを制限する

更新と追加の処理が明確に分けられるようにして、それに対してセキュリティルールを書く方法があります。
元々のデータ構造に依存しないようにするために、ここでは追加日時を持たせることで、更新させることをガードする方法を記載します。

まず追加の時、追加した時間を入れるようにします。

v9の場合
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に載ってたのでいつか対応されるかもしれませんね。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?