サンプルアプリを作って、Firebase について理解を深めてみたシリーズ。3回目はCloud Firestoreです。
Cloud Firestoreとは
Cloud Firestoreは、Firebaseが提供するNoSQL型のデータベース。2019年夏にGAとなり、Firebaseの標準的なデータベースとなりました。Realtime Database よりも高速で多彩なクエリ処理が可能とのことです。
基本的な使い方
Authentication同様、SDKをウェブアプリから読み込みます。
<script src="/__/firebase/7.16.0/firebase-app.js"></script>
<script src="/__/firebase/7.16.0/firebase-firestore.js"></script>
<script src="/__/firebase/init.js"></script>
SDKのロード後、データベースのインスタンスを生成し操作します。
var db = firebase.firestore();
コレクションとドキュメント
Cloud Firebase は、「コレクション」と「ドキュメント」によって構成されます。
コレクションは、ドキュメントを内包するための「フォルダ」のような概念。
ドキュメントは、フォルダ内にある「ファイル」のような概念。
ファイルシステムに近い感覚でデータを管理する仕組みになっています。
ここからは
- test というコレクションの中に、ドキュメントが含まれる
- ドキュメントのIDはそれぞれa, b, xxxx とする
.
└── test
├── id : a
└── id : b
└── id : xxxx
という構造を想定して記述します。
ドキュメントの追加
add もしくはset を使います。
add は、ドキュメント追加時にIDを自動で付与
set は、ドキュメント使い時に任意のIDを指定して付与
という違いがあります。
ドキュメントを追加したときに、同時に時間を書き込みたい場合
firebase.firestore.Timestamp.now()
のように、Timestamp メソッドを使います。
add を使った例。IDは自動的に生成・付与されます。
db.collection("test").add({
name: "Nick",
time: firebase.firestore.Timestamp.now()
})
.then(function() {
console.log("Document successfully written!");
})
.catch(function(error) {
console.error("Error writing document: ", error);
});
set を使った例。IDは「a」に指定されます。すでに「a」というドキュメントが存在した場合、同IDのドキュメントを上書きする形で保存されます。
db.collection("test").doc("a").set({
name: "Nick",
time: firebase.firestore.Timestamp.now()
})
.then(function() {
console.log("Document successfully written!");
})
.catch(function(error) {
console.error("Error writing document: ", error);
});
ドキュメントの削除
delete メソッドを使い、ドキュメントIDを指定して削除します。
[test] コレクションの [b] というIDのドキュメントを削除したい場合、以下のように記述します。
db.collection("test").doc("b").delete().then(function() {
console.log("Document successfully deleted!");
}).catch(function(error) {
console.error("Error removing document: ", error);
});
ドキュメントの呼び出し
ドキュメントのIDを指定して、決めうちで一つのデータを呼び出す場合
db.collection(“test”).doc(“a”
.get().then(function(doc) {
if (doc.exists) {
console.log("Document data:", doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
コレクション「test」内の全ドキュメントを取得して、1つずつ処理する場合
db.collection("test").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
});
コレクション「test」内のドキュメントを取得する時に、「ドキュメントが作成された時間」(time)でソートする場合
db.collection("test").orderBy('time', 'desc')
.get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
});
サブコレクションについて
コレクションの配下に、別なコレクション 「サブコレクション」 という階層構造を持つことができます。
サブコレクションは、「ドキュメント」に紐づく形で生成されます。
- [test] コレクション内にIDが[b]というドキュメントがある
- [b] のドキュメントに[subtest] というサブコレクションを作り、さらにドキュメントを作成する
階層は以下のようになります。
└── test
├── id : a
└── id : b
└── subtest
├── id : c
└── id : d
「ドキュメントの中にサブコレクションが存在する」ところがポイントです。
一般的なファイルシステムの構造と違うため、最初はちょっとややこしいかもしれません。
サブコレクション内のCRUD操作は、階層情報を記述して操作します。
サブコレクションへのデータ書き込み
- test
- b
- subtest
- d
- subtest
- b
のように、サブコレクション内にドキュメント「d」を作る場合、以下のように指定します。
db.collection("test").document(“b”)
.collection(“subtest”).document(“d”)
.add({
name: "Nick",
time: firebase.firestore.Timestamp.now()
})
.then(function() {
console.log("Document successfully written!");
})
.catch(function(error) {
console.error("Error writing document: ", error);
});
サブコレクションの自動生成
サンプルアプリの Cloud Firestore に話を戻します。
今回のサンプルアプリでは
- 不特定多数のユーザーがGoogle認証を通じてアプリを利用できる
- 認証が済んだユーザー分のドキュメントが作成される
というユースケースで設計しています。実際の構造は以下の通りです。
コレクション | ドキュメント | サブコレクション | ドキュメント |
---|---|---|---|
users | {userID} | todos | {documentId} |
挙動としては
- コレクション[users]は初期状態で存在する
- ドキュメントは、Google認証が通ったユーザーのIDごとに自動で決定される
- 各ユーザーのTODOリストデータは、サブコレクション[todos]内に保存される
- [todos]内に、任意のIDを持ったドキュメントが自動生成される
となります。
このように、ユーザーが最初にサインインした瞬間に「ドキュメント」「サブコレクション」を自動で作成したい場合はどうすれば良いか。
Firestoreのドキュメントを探したところ、以下のような説明を見つけました。
…コレクションとドキュメントは Cloud Firestore で暗黙的に作成されます。ユーザーはデータをコレクション内のドキュメントに割り当てるだけです。コレクションまたはドキュメントのいずれかが存在しない場合は、Cloud Firestore によって作成されます…
(「Cloud Firestore データモデル」 より)
つまり、データを書き込む時に、任意のコレクション名・ユーザー名を指定することで、自動的にサブコレクションやドキュメントを生成してくれるようです。
今回ののサンプルアプリの要件
- サンプルアプリのデータモデルに従って、新しいユーザーが認証を通った瞬間に、ユーザーのIDに基づくドキュメントとサブコレクションを作りたい
場合、以下のように書くことで実現できました。
db.collection('users').doc(firebase.auth().currentUser.uid).collection('todos')
.add({
todolabel: newtask,
time: firebase.firestore.Timestamp.now(),
}).then(docRef => {
console.log('Document written with ID: ', docRef.path);
document.getElementById('newtask').value = '';
}).catch(error => {
console.error('Error adding document: ', error);
});
}
Cloud Firestoreのセキュリティルール
Cloud Firestoreのセキュリティは、「ルール」に記述することで設定することができます。基本的な書式は以下の通り。
service cloud.firestore {
match /databases/{database}/documents {
// ここから個別の条件を設定する
}
}
- service cloud.firestore {}
- match /databases/{database}/documents {}
の2つの記述は、おまじないのようなもので、デフォルトで設定する必要があるようです。
match /databases/{database}/documents {
の「/databases/{database}/documents」以下が、各プロジェクトのCloud Firestoreに当たるようです。
ユースケースに従ったルールの設定
今回のサンプルアプリのデータモデル
users - {userId} - todos - {documentId}
に対しては
- 複数人のTODOデータを一つのFirestoreで管理する
- セキュリティを担保するため、個々人のデータを保護する必要がある
状態のため、以下のようなルールで設定します。
- 認証されたユーザーのUIDと、ドキュメントのUIDが一致しないと、データのCRUD操作ができない
コレクション直下の {userId} と、認証済みのユーザーのUIDを比較し、一致した場合のみ操作可能とします。
Cloud Firestoreでは、サブコレクション に対しても個別にセキュリティルールを設定する必要があります。
以上から、今回のルールはこのように書きました。
service cloud.firestore {
match /databases/{database}/documents {
// 認証ユーザーのUIDとドキュメントのIDが一致しないとデータ操作を許可しない
match /users/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
// 各ユーザーのTODOデータの操作も、UIDが一致した場合のみ許可する
match /users/{userId}/todos/{documentId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
ルール設定後は、「ルールプレイグランド」で挙動チェックを行います。
リアルタイムにデータを反映する
[onSnapshot]というメソッドを使い、Firestoreのデータ状況をリッスンすることができます。Firestoreのデータに変更があった場合、ウェブアプリ側の表示にすぐに反映されます。
以下のチュートリアルを学習すると、リアルタイムデータについて詳しく学べるので、一通り写経することをお勧めします。