GoogleのFirebaseが面白いかもしれない 〜Cloud Firestore〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
ファイヤー

この記事を三行にまとめると

今回はCloud Firestoreを使ってみましょう
「コレクション」「ドキュメント」「フィールド」
PHPいらなくね?って改めて思ってしまう
前回はRealtime Databaseを使ったので、今回はもう一つのデータベースであるCloud Firestoreを使ってみましょう。

この記事を書いてる時点ではまだベータ版ですが、Realtime Databaseよりも使い勝手が良さそうな感じがあります。なので今後データベースを使って開発するのであれば、いずれ正式版になるのを期待しつつこっちを使う方が良いかもしれません。Firestoreのコンソール画面にも「次世代のRealtime Database」って書いてあるし。



データの構造について

Cloud Firestoreは「コレクション」「ドキュメント」「フィールド」という三段階でデータを管理するような構造になってます。

前回のRealtime Database同様、Cloud FirestoreもNoSQLのデータベースです。つまりMySQLとは構造が違うわけですが、そうですね……あえてMySQLっぽくイメージするなら、コレクションがテーブルでドキュメントがプライマリーなID、フィールドがレコード、みたいな感じでしょうか。

実際にコンソール画面で見るとこんな風にデータが入ってます。

Firestore

「tweets」というコレクションの中に「1」というユニークなドキュメントがあり、その中にcommentやnameのフィールドがある、みたいなことです。tweetsテーブルのIDが1のデータ、みたいな感覚です。

ただしMySQLと違い、Cloud Firestoreの場合はドキュメントの中にさらにコレクションを作ることができます。テーブルの中のレコードにさらにテーブルを作れる、みたいな。Realtime Databaseでいえばネストを深くするような感じですね。

これを踏まえて実際にデータの読み書きをしてみましょう。



プロジェクトの開始

まずは例によってinitから。

$ firebase init firestore

initするとfirebase.jsonに以下の内容が追加されます。

"firestore": {
  "rules": "firestore.rules",
  "indexes": "firestore.indexes.json"
}

そしてフォルダの中に「firestore.rules」と「firestore.indexex.json」というファイルができていると思います。



データの書き込み

適当なウェブページを作ってデータの書き込みを行ってみましょう。ここでは「firestore.html」というファイルを作ります。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Realtime Database</title>
    <script defer src="/__/firebase/4.6.1/firebase-app.js"></script>
    <script defer src="/__/firebase/4.6.1/firebase-firestore.js"></script>
    <script defer src="/__/firebase/init.js"></script>
  </head>
  <body>
    <button id="write" type="button">書き込みボタン</button>

    <script>
      document.getElementById('write').addEventListener('click', function() {
        firebase.firestore().collection('users').add({
          name: 'あかつき',
          sex: '男性'
        }).then(function(){
          alert('書き込みました!');
        });
      });
    </script>
  </body>
</html>

Realtime Databaseと同じで、firebase-firestore.jsやinit.jsを読み込めば「firebase.firestore()」というインスタンスが使用可能な状態になっています。

データの追加は「collection().add()」というメソッドを使います。collection()の方にコレクション名をセットし、add()で登録したいフィールドをセットします。

デプロイしてアクセス。

$ firebase deploy

https://norm-nois.firebaseapp.com/firestore.html

実際にデータを追加してみるとこんな感じになります。

usersコレクションにデータを追加

コレクションやドキュメントはあらかじめ作っておく必要はありません。add()メソッドを実行すれば自動的に作成してくれます。

例えば先ほどの画像を見ると「users」というコレクションしかありませんが、ここで以下のようなコードを実行すると、自動的に「tweets」というコレクションが作成されます。

document.getElementById('write').addEventListener('click', function() {
  firebase.firestore().collection('tweets').add({
    name: 'あかつき',
    comment: 'こんにちは'
  }).then(function(){
    alert('書き込みました!');
  });
});

コンソールで確認するとちゃんと追加されていますね。

tweetsコレクションにデータを追加



データの上書き

上記の書き方は新規にデータを追加する処理です。

Realtime Databaseの場合は「ref(‘tweets/1’).set()」と書けば、tweetsの1のデータを新規に追加することも上書きすることもできました。でもCloud Firestoreはドキュメントが自動で作成されるので、書き込みボタンを押すたびに新しいドキュメントが作成されてデータが増えていくことになります。

そうならないためには、ドキュメントを指定してデータを更新してあげなければいけない。

例えばtweetsコレクションの1というドキュメントを更新する場合。

firebase.firestore().collection('tweets').doc('1').set({
  name: 'きつかあかつき',
  comment: 'データを上書きします'
});

「doc()」というメソッドでドキュメントを指定すれば、指定したドキュメントの中を更新することができます。

このdoc()は存在しないドキュメントを指定すればエラーになるのかってーと、別にそんなことはありません。なければ新規にドキュメントが作成されます。だからまあ更新とは言いつつも、Realtime Databaseのset()と一緒で、ドキュメントを手動で作成して新規追加もできるって言った方が良いのかな。



ドキュメントの中の特定のフィールドだけを更新したい場合は「update()」というメソッドを使います。

firebase.firestore().collection('tweets').doc('1').update({
  comment: 'コメントだけ上書き'
}).then(function(){
  alert('コメントを書き換えました!');
});

これでドキュメント1のcommentだけが上書きされます。

この場合も、指定したフィールドがないからと言ってエラーにはなりません。なければ新規に追加してくれます。

firebase.firestore().collection('tweets').doc('1').update({
  comment: 'コメントだけ上書き',
  date: '2017-12-19'
}).then(function(){
  alert('コメントを書き換えました!');
});

例えば今、nameとcommentというフィールドしかない状態で上記のコードを実行したら、commentが上書きされてdateというフィールドが新しく追加されます。



データの読み込み

次はデータを読み込みをば。

試しに先ほど追加したtweetsコレクションの1ドキュメントのデータを取ってみましょう。

firebase.firestore().collection('tweets').doc('1').get().then(function(snapshot){
  if(snapshot.exists) {
    var data = snapshot.data();
    document.getElementById('exp_name').innerHTML = data['name'];
    document.getElementById('exp_comment').innerHTML = data['comment'];
  } else {
    alert('データがありません');
  }
});

読み込みは「get()」メソッドを使います。一件だけデータを読み込む場合はこんな感じでコレクションとドキュメントを指定すればオッケーです。

「snapshot.data()」でデータをJSON形式で取得できます。

{comment: "コメントテスト", name: "きつかあかつき"}

あとはこれを自由に使い倒すだけ。

「snapshot.exists」はデータが存在するかどうかを判定するものです。存在しないデータを参照した場合、「snapshot.data()」などを使おうとするとエラーになってしまうので、データが存在するかどうかのif文を噛ませておく必要があります。



じゃあ今度は一件だけじゃなく、条件を指定してコレクションの中から複数のデータを取ってみます。MySQLで言えばwhere句に相当するものですね。

例えば今、usersコレクションに名前、性別、年齢のユーザーデータが入ってるとして、その中から性別が男性のもの(sex:男性)だけを取りたい場合。

firebase.firestore().collection('users').where('sex', '==', '男性').get().then(function(snapshot){
  snapshot.forEach(function(doc){
    console.log(doc.data());
  });
});

「where()」メソッドでwhere句を作るような感じなので、理解はしやすいかもですね。書き方としては「where(‘キー’, ‘条件式’, ‘値’)」という風に書きます。MySQL的に言うと「WHERE sex = ‘男性’」的な。

where()は一度に複数書くことができます。例えばMySQLの「WHERE sex = ‘男性’ and age = 30」みたいなことを書きたければ、where()を二つつなげる。

firebase.firestore().collection('users').where('sex', '==', '男性').where('age', '==', 30).get().then(function(snapshot){
});

こんな感じです。ちょっと横長になっちゃいましたかね。こういうメソッドチェーンみたいな書き方ではなく、変数を使って複数行に分けて書いても良いです。

query = firebase.firestore().collection('users');
query = query.where('sex', '==', '男性');
query = query.where('age', '==', 30);
query.get().then(function(snapshot){
  console.log(snapshot);
  snapshot.forEach(function(doc){
    console.log(doc.data());
  });
});

サンプルページに絞り込みの条件をいくつか指定できるようにしたので、よかったらいろいろいじってみてくださいな。



カスタムインデックスについて

さっきは「where(‘sex’, ‘==’, ‘男性’)」「where(‘age’, ‘==’, 30)」みたいに、両方ともイコールの条件でした。

この場合は特に問題ないのですが、片方はイコールで、もう片方は以上とか以下みたいな条件の場合。例えば「where(‘sex’, ‘==’, ‘男性’)」「where(‘age’, ‘>=’, 30)」みたいな条件でデータを取得したい場合は、インデックスを作成していないとエラーになってしまいます。実際やってみると「The query requires an index.」みたいなエラーが出る。

インデックスはコンソール画面から作成できます。

カスタムインデックス作成画面

とりあえず手動でインデックスを追加してみましょう。

インデックスを手動で追加

sexとageの複合インデックスを作成します。

インデックスができました

これで「where(‘sex’, ‘==’, ‘男性’)」「where(‘age’, ‘>=’, 30)」の条件でもデータの取得が可能になります。



ORDERやLIMIT

whereだけじゃなく、MySQLでいうORDER BYやLIMITなども使えます。

users = firebase.firestore().collection('users');
query = users.where('age', '>=', 30).orderBy('age', 'desc').limit(3);
query.get().then(function(snapshot){
  console.log(snapshot);
  snapshot.forEach(function(doc){
    console.log(doc.data());
  });
});

これは年齢が30歳以上の人たちを年齢の高い順に3件取得する場合です。「WHERE age >= 30 ORDER BY age DESC LIMIT 3」的な。まんま「orderBy()」や「limit()」というメソッドがあるので、それ使えばオッケーです。orderBy()の第二引数を省略した場合はascになります。



データの削除

データの削除は大きく分けて三パターンあります。

ドキュメントを削除する場合は「delete()」メソッドを使います。

firebase.firestore().collection('tweets').doc('1').delete().then(function(){
  alert('ドキュメントを削除しました!');
});

これでtweetsコレクションの1ドキュメントのデータを削除できます。



ドキュメントの中の特定のフィールドを削除する場合は上書きの時と同じく「update()」メソッドを使います。

firebase.firestore().collection('tweets').doc('1').update({
  comment: firebase.firestore.FieldValue.delete()
}).then(function(){
  alert('コメントを削除しました!');
});

「FieldValue.delete()」というメソッドを使うと指定したフィールドを削除できます。上記のコードはドキュメントの中のコメントフィールド(comment)を削除しています。

サンプルページで「コメント削除」ボタンを押した後、読み込みボタンをもう一度押してもらえば削除されたのが確認できると思います。一言の方が「undefined」になる。

複数のフィールドを消すこともできます。

firebase.firestore().collection('tweets').doc('1').update({
  name: firebase.firestore.FieldValue.delete(),
  comment: firebase.firestore.FieldValue.delete()
}).then(function(){
  alert('コメントを削除しました!');
});

これなら「name」「comment」の二つのフィールドが削除されます。



最後にコレクションを削除する場合。現時点ではコレクションそのものを削除するメソッドはないっぽい。

公式のリファレンスを見ると何やら小難しいコードがわちゃわちゃ書かれてるんですが、ようは対象のコレクションの中にあるドキュメントを全て削除すれば、コレクション自体もなくなるみたいです。

コレクションの削除

ドキュメントの数が多い場合はメモリ不足なんかを気にしなきゃいけないみたいですが、データが少ないうちはこんな感じのコードでもコレクションを削除できます。

$collection = firebase.firestore().collection('tweets');
$collection.get().then(function(snapshot){
  snapshot.forEach(function(doc){
    $collection.doc(doc.id).delete();
  });
});

tweetsコレクションのデータを一通り取得して、片っ端から削除してます。「doc.id」でドキュメントのIDが取れるので、それを使ってドキュメントの削除をやればコレクションの中が空になり、自動的にコレクションもなくなります。






データの扱い方はRealtime Databaseとそんなに変わらないように思いますが、Cloud Firestoreの方はフィールドにstringやtimestampなどの型を指定することもできます。array型も使えるってのは便利かもしれない。

あとカラムを意識しなくて良いってのも、意外と楽かもしれないなーと感じました。MySQLだとテーブルの中のカラムは一律だから必要ないカラムにはnullとか空のデータを入れておかなきゃいけないし、新しい項目を増やすたびにカラムを追加する作業も必要になりますけど、Realtime DatabaseやCloud Firestoreはドキュメントごとにフィールドを自由に作れるから、不要なフィールドは作成すらしなくて良い。テーブルの中でレコードごとに好きにカラムを作成できるみたいなイメージですね。

それにしても、やはりここまでできるともうPHPいらなくね?って改めて思ってしまいますね。コレクションどうしをリレーションできなくても、これだけ使えればHTMLとCSS、javascriptだけでだいたい何とかなる気がする。



その他のFirebaseに関する記事はこちら

 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください