NoSQLで悩ましいインデックスとアクセス権限管理を、Firestoreのサブコレクションで実装する

概要

FirestoreはNoSQLの中でもデータ構造に特徴があります。 本記事では私自身が実際に設計したデータ構造がサブコレクションでどう変わったかをみていくことで、サブコレクションでできることを書いていきたいと思います。

開発環境

Firestoreについて

Googleが提供しているNoSQLのマネージドサービスです。 他のNoSQLと比較してWeb, Mobileからローカルデータベースのように利用できることやデフォルトで単一フィールドのインデックスが作成されていることが特徴です。

データ構造

今回はチャット機能のデータを考えてみます。 登場人物は以下になります。

  • ユーザー
  • チャットルーム
  • 投稿(コメント、画像を含む)

チャットルームには2人以上のユーザーが参加することができます。

f:id:kaminashi-developer:20210125093352p:plain
チャット機能のデータ構造

Before

改善する前の設計方法を書いていきます。

Posts コレクションに Rooms のIDを持たせてクライアントで結合していました。

f:id:kaminashi-developer:20210222101821p:plain
改善前のPostsコレクション

特定の Rooms に含まれる投稿を作成順に取得するときは以下のようになります。

const db = firebase.firestore()
const collection = db.collection('posts')

collection
  .where('roomID', '==', '0ff04b17-5e8f-11eb-ac15-067462cbca66')
  .orderBy('createdAt', 'asc')
  .get()
  .then((docs) => {
    docs.forEach((doc) => {
      const post = doc.data() // { text: 'こんにちは'... }
    })
  })

この場合、以下のような課題がありました。

  1. 投稿を取得するために roomID および createdAt のフィールドを条件にしているため複合インデックスを作成する必要がある
  2. Posts コレクションに対してセキュリティルールを作る情報が足りていない

1について、Firestoreはデフォルトで単一フィールドのインデックスが有効になっています。しかし、複数条件に対してはインデックスの条件ををあらかじめ指定しておく必要があります。そのため今後新しく検索したいパターンが増えるたびに複合インデックスを追加する必要があります。

2について、 Posts コレクションはチャットルームに参加しているユーザーデータを持っていません。そのためセキュリティルール上で参加者以外閲覧不可のような設定ができなく、Rooms に含まれるユーザーデータを複製して持たせないといけないです。複製するとユーザーが Rooms から抜けたりしたときに、 Rooms 内の全 Posts コレクションのユーザーデータを更新する必要があります。

これらを改善するためにサブコレクションを使いました。

After

Beforeでは RoomsPosts のパスはそれぞれ /rooms/posts とそれぞれ別のルートから作成していました。サブコレクションを使うとパスは /rooms/{roomID}/posts として作成します。

f:id:kaminashi-developer:20210125093445p:plain
改善後のPostsコレクション

改善前でも書いた Posts コレクションを取得する処理は以下のようになります。

const db = firebase.firestore()
// コレクション作成時にroomsのIDを指定
const collection = db.collection('rooms/0ff04b17-5e8f-11eb-ac15-067462cbca66/posts/')

collection
  .orderBy('createdAt', 'asc')
  .get()
  .then((docs) => {
    docs.forEach((doc) => {
      const post = doc.data() // { text: 'こんにちは'... }
    })
  })

Rooms のIDがWhere文からCollectionの取得条件になっているため単一フィールドのインデックスで取得できます。 また Rooms に適応するセキュリティルールを Posts に反映できるので、ユーザーデータを Posts に持たせる必要がなくなりました。

投稿の追加は以下のようにできます。

const db = firebase.firestore()
const collection = db.collection('rooms/0ff04b17-5e8f-11eb-ac15-067462cbca66/posts/')

const uuid = uuidv4()
collection
  .doc(uuid)
  .set({
    id: uuid,
    ownerID: currentUser.uid,
    text: 'はじめまして',
    type: 'message'
  })

まとめ

サブコレクションを使うことで1対多のデータ構造のインデックスおよびセキュリティルールを簡略にできました。 最初に設計したIDによる結合方法とサブコレクションはデータ構造に応じて柔軟に選択することができます。

今後、設計時にセキュリティールールやインデックスも考慮して拡張しやすい構造を作っていきたいです。