今回は、すでに存在するドキュメントの作成を禁止する方法を紹介します。
なぜUpdateでなく**「すでに存在するドキュメントの上書き作成」**なんて回りくどい言い方をするかというと、
- クライアントサイドのsetメソッドでの同一パスへの書き込みはUpdate扱いとなる
- 部分的なフィールドのUpdateは許可したい
といった背景があります。
セキュリティルールで意図しない書込みを防ぐ
Firebase Firestoreのセキュリティルールでは、match文でドキュメントパスを指定し、
allow式でドキュメントのread、create 、update、delete権限を指定することができます。
※以下、権限に関するcreate等は太字で表現します。
参考:【改訂版】 Firebase Cloud Firestore rules tips
setメソッドでの同一パスへの書き込みはUpdate扱いとなる
クライアントからのFirestoreドキュメントの指定パスへの書込み方法の代表的なものとして、set
メソッドがあります。
参考:Firebase firestore:Writing Data
これは、従来のデータベースのinsertに近いイメージですが、merge
オプションをセットすることでupsertのような、update、もしなければinsert
といった挙動をさせることができます。
よって、Firestoreドキュメントの指定パスへの書込み方法の分類はmerge
オプションを区別すると
set without merge
、set with merge
、update
の3種類となります。
(今回はmergeFieldsオプションについてはmergeとして扱います)
この3つには下記のような違いがあります。
-
set without merge
: ドキュメントそのものをupdate、もし存在しないようならcreate -
set with merge
: ドキュメントのフィールドのupdate、 もし存在しないようならcreate -
update
: ドキュメントのフィールドのupdate、 もし存在しないようなら失敗する
//すべて同期的に動作するものと仮定したコードです
let firestore = Firestore.firestore()
//新規documentIDでReferensePathを作成
let userRef = firestore.collection("users").document()
let data1 = ["name":"userA"]
//ドキュメントが存在しないため失敗する
userRef.updateData(data1)
//ドキュメントが存在しないのでcreateされる
userRef.setData(data1, merge: true)
/*
user > {documentID} > ["name":"userA"]
*/
//ドキュメントそのものがupdateされるためnameフィールドは消える
let data2 = ["age": 21]
userRef.setData(data2)
/*
user > {documentID} > ["age": 21]
*/
//ドキュメントが存在するためフィールドがupdateされる。ageフィールドは消えない
//userRef.updateData(data1, merge: true)と同じ挙動
userRef.setData(data1, merge: true)
/*
user > {documentID} > ["name":"userA", "age": 21]
*/
set without mergeはすでに存在するドキュメントを無視する
まだ登録されてないuserIDのprofileだけ登録したい等の目的でcreateを目的としたset
メソッドを使用して書き込み処理を行なっても、すでに存在するdocumentPathに対してはupdateの挙動となってしまいます。
なので、
- すでに存在するドキュメントの上書き作成を防ぎたい
- 通常の部分的なフィールドの更新は許可したい
といった意図をセキュリティルールで実現するには、update権限のルールを工夫する必要があります。
1度しかデータを作成できないようにする
方法としては、初回作成時にしか書き込まないcreatedAt
のようなフィールドを利用します。
具体的には
- データ作成時は必ず
createdAt
を書き込むこと - 下記のようなルールで
createdAt
の更新のみを禁止すること
によって、すでに存在するドキュメントにcreate
しようとした処理を失敗させることができます。
match /users/{userID} {
allow read: if request.auth.uid != null;
allow create: if request.auth.uid == userID
&& request.resource.data.createdAt != null
allow update: if request.auth.uid == userID
&& request.resource.data.createdAt == resource.data.createdAt;
ちなみに
request.resource.data.createdAt == resource.data.createdAt
のようなルールをallow文に記述することで、フィールドの変更を制限することができます。