Firestoreについて

昨日(2017-10-04)、Googleが提供しているFirebaseに、新しいデータベースサービスCloud Firestoreが追加されました。以前からFirebaseには興味がありで、ちょうど良い機会だったのでCloud Firestoreを触ってみました。
最終的にCloud Firestoreを使ったTodoアプリの作成をご紹介いたします。
慣れないと不安なセキュリティに関する部分も簡単ではありますが解説しますので、Google Firebaseに初めて触れる方がいましたら、私も初心者ですので一緒に頑張っていきましょう。

Firestoreのドキュメント

基本的にはGoogle公式のCloud Firestoreドキュメントの通りに実装していきます。現在は英語ですが、コード等に迷ったらこちらをご参照ください。

Cloud Firestoreの準備

Firebaseに移動し、右上のログインボタンからログインしてください。

Firebase Cloud Firestoreのログイン画面

ログインしたら、右上の「GO TO CONSOLE」をクリックし、Firebaseのコンソールに移動します。
プロジェクトの一覧が表示されますので、「プロジェクトを追加」から新規でプロジェクトを作成してください。

Firebase Cloud Firestoreのプロジェクト作成画面

プロジェクトが作成されると、プロジェクトのコンソールに移動します。最初はCloud Firestoreが使えませんので、使えるように有効化します。
左メニューの「Database」をクリックし、続いてDatabaseの選択画面が表示されますので、右側の「FIRESTOREベータ版を試してみる」をクリックします。

Cloud Firestoreの有効化手順 Databaseをクリックして、Firestoreベータ版を試してみる をクリック

Cloud Firestoreのセキュリティルールを選択するダイアログが表示されますので、テストモードを選択したら、右下の「有効にする」ボタンをクリックしてCloud Firestoreを有効化してください。
※次の記事でCloud Firestoreのセキュリティルールを設定してログインしているユーザーだけがアクセスできるようにします。

Firestoreが有効化されると、コンソールの左上に「Cloud Firestore ベータ版」のセレクトオプションが表示されますので確認してください。

Cloud Firestoreの有効化確認 Firebaseのコンソール画面

以上でCloud Firestoreを使う準備ができました。

Cloud Firestoreを使ったサンプルアプリの作成

この記事ではCloud Firestoreを使った、「書き込み」「読み込み」「更新」「削除」「変更の監視」をおこないます。
まずはCloud Firestoreをインストールする必要がありますので、その方法をご紹介します。
ページの最後にソースコードの全景を掲載していますので、ローカル開発環境にてご確認ください。

FirebaseとCloud Firestoreのインストール

<script src="https://www.gstatic.com/firebasejs/4.5.0/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.5.0/firebase-firestore.js"></script>
<script>
    const config = {
        apiKey: "ウェブAPIキー",
        authDomain: "オースドメイン",
        projectId: "プロジェクトID",
    }
    //- Firebaseのインスタンス初期化
    firebase.initializeApp(config);
</script>

configの「apiKey」「authDomain」「projectId」は、Firebaseコンソールメニューの「Overview」→「ウェブアプリにFirebaseを追加」から確認できます。
ダイアログに表示されているコードをそのままコピーペーストすると楽です。

Cloud Firestoreのインストール方法 Overview をクリック、ウェブアプリにFirebaseを追加をクリックし、表示されるダイアログのソースコードをコピーペースとする。

NPMを使う場合は

$ npm i -D firebase@4.5.0

でダウンロードし、手動でインポートします。

const firebase = require("firebase");
require("firebase/firestore");

さて、Firebaseのインストールと初期化ができましたので、次にCloud Firestoreのインスタンスを生成します。

//- Firestoreのインスタンス生成
const db = firebase.firestore();

これまでのコードは次の通りですが、後でボタンやフィールドを操作するので、DOMの読み込みが終わってからCloud Firestoreのインスタンスを生成するよう、DOMContentLoadedイベントを待って処理しています。
また、アジェンダも番号の通りです。

<script src="https://www.gstatic.com/firebasejs/4.5.0/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.5.0/firebase-firestore.js"></script>
<script>
  /*---------------------------------------*/
  /* 1.firebase初期化 */
  /*---------------------------------------*/
  const config = {
    apiKey: "ウェブAPIキー",
    authDomain: "オースドメイン",
    projectId: "プロジェクトID",
  }
  //- Firebaseのインスタンス初期化
  firebase.initializeApp(config);

  /*---------------------------------------*/
  /* 3.自分のアプリを初期化 */
  /*---------------------------------------*/
  function initApp(){

      /*---------------------------------------*/
      /* 4.Firestoreインスタンス生成 */
      /*---------------------------------------*/
      const db = firebase.firestore();

      /*---------------------------------------*/
      /* 5.DOM定義 */
      /*---------------------------------------*/

        /*---------------------------------------*/
        /* 6.ドキュメントに接続 */
        /*---------------------------------------*/

        /*---------------------------------------*/
        /* 7.データの追加 */
        /*---------------------------------------*/

        /*---------------------------------------*/
        /* 8.データの読込 */
        /*---------------------------------------*/

        /*---------------------------------------*/
        /* 9.データの更新 */
        /*---------------------------------------*/

        /*---------------------------------------*/
        /* 10.データの削除 ドキュメント */
        /*---------------------------------------*/

        /*---------------------------------------*/
        /* 11.データの監視 */
        /*---------------------------------------*/
  }


  /*---------------------------------------*/
  /* 2.DOMの読み込みを待って */
  /*---------------------------------------*/
  document.addEventListener('DOMContentLoaded',initApp);

これでCloud Firestoreを操作する準備が整いました。
次に、ボタンやらインプットフィールドやらIを用意します。

ボタンやインプットフィールドのHTML構造

今回のサンプルでは次のHTML構造となっています。

<h1>Message is: <span id="output"></span>
  <button id="loadBtn">表示</button>
</h1>
<input id="messageFld" type="text">
<button id="saveBtn">保存</button>
<button id="updateBtn">更新</button>
<button id="deleteBtn">削除</button>

ちょっと歪んでいますので、適宜整形してください。
次に、後でjsで色々と操作しますので、予め定義しておきます。

/*---------------------------------------*/
/* 5.DOM定義 */
/*---------------------------------------*/
const $output = document.querySelector('#output');
const $messageFld = document.querySelector('#messageFld');
const $saveBtn = document.querySelector('#saveBtn');
const $loadBtn = document.querySelector('#loadBtn');
const $updateBtn = document.querySelector('#updateBtn');
const $deleteBtn = document.querySelector('#deleteBtn');

コレクション/ドキュメントに接続する

Cloud Firestoreのインスタンスを生成しましたので、次は利用するドキュメントに接続する必要があります。
Cloud Firestoreではコレクションとドキュメントという概念があり、それぞれ次のようなイメージになっています。

  • ドキュメント
    フィールド(例、名字、名前、年齢、性別、、、)を含んだレコードのことで、一意の名前によって定義されます。
  • コレクション
    ドキュメントを含んだ単なるコンテナです。例えばユーザー情報としてのUserコレクションがあったとすると、Aさんの情報(ドキュメント),Bさんの情報(ドキュメント)、、、といった感じで、ユーザーのドキュメントを内包しているデータベースとなります。ただ、なんでも含める事ができるため、相当自由なコンテナです。
    詳しくはCloud Firestore の Data Modelをご確認ください。

基本的にCloud Firestoreのデータを操作する場合は、[コレクションに接続] → [その中のドキュメントを指定] という操作(インスタンス)が必要になります。
今回はメッセージの書き込みや削除などを操作しますので、sampleコレクションの中のmessageというドキュメントに接続することにします。

ドキュメントへの接続は

/*---------------------------------------*/
/* 6.ドキュメントに接続 */
/*---------------------------------------*/
const docRef = db.collection('sample').doc('message');

のように、collection()メソッドによって、コレクションに接続したあと、doc()メソッドによって、ドキュメントIDを指定してドキュメントに接続します。
ドキュメントは更にコレクションを持つことができますので、この構造を繰り返すことで、深いデータ構造でも同様にアクセスすることが可能です。

doc()メソッドは、コレクション/ドキュメント/コレクション/ドキュメント/…という形式で、スラッシュを使ったパスでもドキュメントの指定が可能なので、以下でもokです。Googleもこちらを推奨しているようです。

/*---------------------------------------*/
/* 6.ドキュメントに接続 ※パスで指定する場合 */
/*---------------------------------------*/
const docRef = db.doc('sample/message');

Firebaseのコンソールでは以下のようにコレクションを用意しています。

Cloud Firestoreのデータ構造 sampleコレクション → messageドキュメントにvalueフィールドを用意

※なお、コレクションやドキュメントを予め用意していなくても、存在していない場合は自動で作成されます。

これでdocRef変数にドキュメントを操作するためのインスタンスが生成されましたので、以降はdocRefからデータを操作していくことになります。

書き込む

書き込むには、set()とadd()メソッドがあります。両者の違いは、set()メソッドはドキュメントIDに任意の名前をつける事ができる(指定する必要がある)のに対し、add()メソッドはIDを自動生成して追加させることができるという点です。
要は、連想配列やオブジェクトのような構造を望むのならset(),配列の構造ならばadd()で良いよといった感じでしょうか。今回は主にset()を使ってデータの書き込みを行います。

/*---------------------------------------*/
/* 7.データの追加 */
/*---------------------------------------*/
$saveBtn.addEventListener('click', event => {
    docRef.set({
        value: $messageFld.value
    }).then(()=>{
        console.log('success');
    }).catch(error => {
        console.error(error);
    })
});

試しに,インプットフィールドに’Hellow Firestore!’と書き込んで、保存ボタン($saveBtn)をクリックしてみてください。Firestore sampleコレクションのmessageドキュメントのvalueに値が格納されていれば書き込みは成功です。

FirestoreのsampleコレクションMessageドキュメントのvalueフィールドに Hellow Firestoreのテキストが格納されている様子

データの読み込み

データの読み込みも簡単です。docRefにはmessageドキュメントのインスタンスが定義されているため、get()メソッドによって参照するだけです。Promiseとしてドキュメントのオブジェクトが帰ってきます。
ドキュメントオブジェクトが帰ってくると、そのプロパティに存在チェック用のプロパティexistsが設定されているため、existsがtrueの時だけ表示するようにします。

/*---------------------------------------*/
/* 8.データの読込 */
/*---------------------------------------*/
$loadBtn.addEventListener('click', event => {
    docRef.get().then( doc => {
        if(doc && doc.exists){
            $output.innerHTML = doc.data().value;
        }
    }).catch(error => {
        console.log(error);
    });
})

試しに、新しくメッセージを保存して、「表示」ボタンを押してみてください。タイトルの横に保存されているメッセージが表示されると思います。

データの更新

データの更新はupdate()メソッドによって操作します。set()やadd()は新しいデータを追加するときに主に使われます(set()は上書きでも使います)が、update()はドキュメントが存在している場合のみとなります。
このサンプルではほぼset()と同様の動作になりますが、update()メソッドはフィールドデータの削除にも使うため、データの存在チェック機能もあるとなると、使用頻度は高くなりそうです。

/*---------------------------------------*/
/* 9.データの更新 */
/*---------------------------------------*/
$updateBtn.addEventListener('click', event => {
    docRef.update({
        value: $messageFld.value
    }).then(()=>{
        console.log('updated');
    }).catch(error => {
        console.error(error);
    });
});

データの削除

データの削除は、フィールドとドキュメントの削除では操作が異なります。
フィールド(ドキュメントのフィールド)を削除する場合は、update()メソッドを使って、対象のフィールドにFieldValue.delete()を当てます。

/*---------------------------------------*/
/* 10.データの削除 フィールドの削除 */
/*---------------------------------------*/
$deleteBtn.addEventListener('click', event => {
    //- 基本的にフィールドの値を消す場合は、FieldValue.delete()を使用してupdate()する。
    docRef.update({
        value: firebase.firestore.FieldValue.delete()
    }).then(()=>{
        console.log('deleted field');
    }).catch(error => {
        console.error(error);
    });

    //- ドキュメント自体をまるごと削除したい場合はdelete()を使う
    // docRef.delete().then(()=>{
    //  console.log('deleted');
    // }).catch(error => {
    //  console.log(error);
    // });
});

ドキュメント自体を削除したい場合は、ドキュメントインスタンスに対してdelete()メソッドを実行します。

/*---------------------------------------*/
/* 10.データの削除 ドキュメントの削除 */
/*---------------------------------------*/
$deleteBtn.addEventListener('click', event => {
    //- ドキュメント自体をまるごと削除したい場合はdelete()を使う
    docRef.delete().then(()=>{
       console.log('deleted');
    }).catch(error => {
        console.log(error);
    });
});

ドキュメントを削除したいのか、ドキュメントの中の値を削除(初期化)したいのかで、メソッドも変わりますので留意する必要がありそうです。

データの監視

データの変更があるたびに、毎回「表示」ボタンを押して確認するのは非常に手間です。よってドキュメント(コレクションでも)のフィールドの値を監視し、値に変更があれば自動で表示されるようにしてみましょう。
ドキュメント(コレクション)の監視は、onSnapshot()メソッドによって実装できます。

/*---------------------------------------*/
/* 11.データの監視 */
/*---------------------------------------*/
function getRealtimeUpDates() {
    docRef.onSnapshot({includeMetadataChanges: true}, doc => {
        if(doc && doc.exists){
            $output.innerHTML = doc.data().value;
        }
    })
}
getRealtimeUpDates();

Cloud Firestoreのデータの監視は便利ですが、多くの人が同時にアクセス&変更した場合、更新がCloud Firestoreに反映されていない状況も考えられます。また、オンラインでの利用を考慮されているFirebaseだと、このあたりの処理は大切です。
そこで、includeMetadataChangesオプションをtrueに設定しておくことで、ローカルでの変更を検知したタイミングを監視してくれるようになり便利です。

HTML全景

これまでCloud Firestoreの基本的な機能を実装してみました。HTMLの全景は次のとおりです。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Google Firestore Lab</title>
  </head>
  <body>
    /*---------------------------------------*/
    /* HTML構造 */
    /*---------------------------------------*/
    <h1>Message is: <span id="output"></span>
      <button id="loadBtn">表示</button>
    </h1>
    <input id="messageFld" type="text">
    <button id="saveBtn">保存</button>
    <button id="updateBtn">更新</button>
    <button id="deleteBtn">削除</button>

    <script src="https://www.gstatic.com/firebasejs/4.5.0/firebase.js"></script>
    <script src="https://www.gstatic.com/firebasejs/4.5.0/firebase-firestore.js"></script>
    <script>
      /*---------------------------------------*/
      /* 1.firebase初期化 */
      /*---------------------------------------*/
      const config = {
        apiKey: "ウェブAPIキー",
        authDomain: "オースドメイン",
        projectId: "プロジェクトID",
      }
      //- Firebaseのインスタンス初期化
      firebase.initializeApp(config);


      /*---------------------------------------*/
      /* 3.自分のアプリを初期化 */
      /*---------------------------------------*/
      function initApp(){


            /*---------------------------------------*/
            /* 4.Firestoreインスタンス生成 */
            /*---------------------------------------*/
            const db = firebase.firestore();


            /*---------------------------------------*/
            /* 5.DOM定義 */
            /*---------------------------------------*/
            const $output = document.querySelector('#output');
            const $messageFld = document.querySelector('#messageFld');
            const $saveBtn = document.querySelector('#saveBtn');
            const $loadBtn = document.querySelector('#loadBtn');
            const $updateBtn = document.querySelector('#updateBtn');
            const $deleteBtn = document.querySelector('#deleteBtn');


            /*---------------------------------------*/
            /* 6.ドキュメントに接続 */
            /*---------------------------------------*/
            const docRef = db.collection('sample').doc('message');
            //- const docRefh = db.doc('sample/message');


            /*---------------------------------------*/
            /* 7.データの追加 */
            /*---------------------------------------*/
            $saveBtn.addEventListener('click', event => {
                docRef.set({
                    value: $messageFld.value
                }).then(()=>{
                    console.log('success');
                }).catch(error => {
                    console.error(error);
                })
            });


            /*---------------------------------------*/
            /* 8.データの読込 */
            /*---------------------------------------*/
            $loadBtn.addEventListener('click', event => {
                docRef.get().then( doc => {
                    if(doc && doc.exists){
                        $output.innerHTML = doc.data().value;
                    }
                }).catch(error => {
                    console.log(error);
                });
            })


            /*---------------------------------------*/
            /* 9.データの更新 */
            /*---------------------------------------*/
            $updateBtn.addEventListener('click', event => {
                docRef.update({
                    value: $messageFld.value
                }).then(()=>{
                    console.log('updated');
                }).catch(error => {
                    console.error(error);
                });
            });


            /*---------------------------------------*/
            /* 10.データの削除 ドキュメント */
            /*---------------------------------------*/
            $deleteBtn.addEventListener('click', event => {
                docRef.update({
                    value: firebase.firestore.FieldValue.delete()
                }).then(()=>{
                    console.log('deleted field');
                }).catch(error => {
                    console.error(error);
                });
            });


            /*---------------------------------------*/
            /* 11.データの監視 */
            /*---------------------------------------*/
            function getRealtimeUpDates() {
                docRef.onSnapshot({includeMetadataChanges: true}, doc => {
                    if(doc && doc.exists){
                        $output.innerHTML = doc.data().value;
                    }
                })
            }
            getRealtimeUpDates();
      }


      /*---------------------------------------*/
      /* 2.DOMの読み込みを待って */
      /*---------------------------------------*/
      document.addEventListener('DOMContentLoaded',initApp);


    </script>
  </body>
</html>

現時点だと、Cloud Firestoreのセキュリティルールを設定していない(だれでも自由にデータを操作できる)状態ですので、必ずローカル開発環境だけで確認してください。次の記事にて、認証処理やセキュリティルールについて、簡単ではありますが説明いたします。

所感

初めてGoogleの新データベースサービス Cloud Firestore を使ってみて感じたことは、気軽にデータを操作でき、かつ、ウェブサービスで必要とされる機能が用意されているので、本当にサーバーレスでウェブサービスを実装できそうということです。料金も格安ではないでしょうか。
気になったポイントとしては、レスポンスが数秒(1秒~4秒)くらいかかる、データの操作に対してもPromiseが返ってくるのが遅いという点です。
ただ、そういったデメリットを補っても非常に高性能だと思います。近いうちに大規模なアプリがCloud Firestoreをベースにして公開されてもおかしくはないサービスだと感じました。  

しかし、さわっていて本当に楽しいサービスです。

NEXT ARTICLES

次はCloud Firestoreのセキュリティルールや認証(Authentication)サービスを実装し、セキュリティについて少しだけ解説致します。
近いうちに記事をまとめますが、不定期となりますことご容赦ください。
へばな!