VIVITABLOG

VIVITAで活躍するメンバーの情報発信サイト

NoSQL(firestore)でRDBのテーブル結合の様な使い方を試してみた

NoSQLではRDBであるようなテーブル結合の様なデータ取得方法はできないと思っていましたが、 FirebaseのCloud Firestoreにはドキュメントのフィールドのデータ型に"reference"というものがあり、 他で定義されているドキュメントを参照できる事を知ったので試してみました。

以前にサンプルとして作っていたチャットアプリを改変してreferenceを試してみます。 ユーザーのマスタデータを事前に準備し、 メッセージのトランザクションデータ内にユーザー名を参照するreference型のフィールドを定義するようにしてみます。 f:id:itamoto:20190828111924p:plain RDBのER図でいうとこんな感じで。

ユーザーのマスタデータは手動で作ります。

f:id:itamoto:20190828105907p:plain
ユーザーのマスタデータ
メッセージデータが入るコレクションも作っておきます。

早速実装内容 いつもどおりfirebaseに接続してfirestoreを取得し、chat_user以下のDocumentSnapshotを配列に保持しておきます。

firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();

const userCollection = db.collection("chat_user")
userCollection.orderBy('name', 'asc').onSnapshot((qs) => {
  qs.forEach(result => {
    this.userList.push(result);
  })
});

このユーザーのリストデータを使ったView部分は以下のようにしました。(Vue.jsを使っています)

    <select v-model="userRef">
      <option v-for="u in userList" v-bind:value="u.ref">{{u.data().name}}</option>
    </select>

DocumentSnapshotのプロパティからDocumentReferenceを取得したものをhtmlのselectで選択した結果にバインドしました。 userRefという変数にDocumentReferenceが格納されるようになります。

続いて投稿メソッドを作成します。 userというフィールドに上記で保持しているユーザーデータのDocumentReferenceをそのまま登録します。

methods: {
    sendMessage: function() {
      db.collection("chat_messages").add(
        {
          user: this.userRef,
          message: this.message,
          created: new Date().getTime()
        }
      )
    }
  },

このメソッドを呼び出して登録されたデータはこのようになりました。 f:id:itamoto:20190828120718p:plain リファレンス型のデータを投入するのはすごく楽です。

次にメッセージデータを画面上に展開してみます。

db.collection("chat_messages").orderBy('created', 'desc').onSnapshot((qs) => {
  this.messageList = [];
  let msgCount = 0;
  qs.forEach(async result => {
    const addData = {'userId': result.id,'userName': '', 'message': result.data().message, 'created': result.data().created}
    this.messageList.push(addData);

    const userSnapshot = await result.data().user.get();
    this.messageList[msgCount].userName = userSnapshot.get("name");
    msgCount++;
  })
});

登録された日時の降順でデータを取得し、その内容をmessageListという変数に格納します。 userフィールドにはDocumentReferenceが入っているので、getメソッドでDocumentSnapshotを取得してから参照先のnameフィールドの値を取得という処理になっています。

View側での表示はこのようにしました。

    <table border="1">
      <tr v-for="(msg, key, index) in messageList" v-bind:key="index">
        <td>{{ msg.created | moment}}</td>
        <td>{{ msg.userName }}</td>
        <td>{{ msg.message}}</td>
      </tr>
    </table>

表示結果は以下のようになりました。 f:id:itamoto:20190828122903p:plain

Firestoreのchat_userのnameを直接変更したら画面上の表示も変更されることを確認できました。 ちなみに、reference型の参照先のデータが変更されたのはメッセージデータが変更されたという検知がされないようで、ブラウザのリロードは必要となりました。

まとめ

このreference型を利用すれば、これまでNoSQLだから冗長なデータを作成をしていた部分もすっきりしそうです。

ただし、参照先のデータを取得するには再度取得してくる処理を記述する必要もあり、 RDBよりフロント側の実装コストはかかってしまいそうです。 ユーザー側で全く更新されることのないマスタデータの場合にはreference型を利用せずに 非正規化したデータ設計もありかと思います。