Firebase④ Cloud FireStore

Cloud FireStoreとは

Cloud FireStore(以下FireStore)とは、FireBaseの提供するドキュメント指向型 のNoSQLデータベースです。 NoSQLとしての特徴としてのスキーマレススケーラブルといった特徴のほかにリアルタイムアップデートセキュリティルールオフラインサポートといった独自の特徴を備えており、特にバックエンドを介さずにクライントサイドから直接操作できるという点が大きなポイントです。 また、β版から正式リリースされたのが2019年2月ということもあり、比較的新しい技術です。

RealTimeDatabaseとの違い

FireStoreの登場時期がつい最近ということに触れましたが、FireStoreが登場する以前はRealTimeDataBaseが使用されていました。FireStoreは、RealTimeDatabaseの特徴を受け継いだデータベースであり、RealTiemDatabaseの弱点であったデータモデルを改善したりクエリを強化したりなどより使いやすくなっています。ですから、新たにプロジェクトを開始する場合にはほとんどの場合FireStoreを利用するべきです。

以下は、FireStoreとRealTimeDatabaseの比較です。

RealTimeDatabase FireStore
データモデル JSON ドキュメントのコレクション
クエリ ・制限あり(フィルタリングと並び替えをの両方を同時に行うことはできない)・取得したデータのすべての子ノードを返す・JSONツリーの個々のノードまでアクセスできる・インデックスを必要としないが、データセットが大きくなるにつれて特定のクエリのパフォーマンスは低下する・ ・一つのクエリでフィルタを組み合わせたり、フィルタリングと並び替えを同時に行うことができる・特定のドキュメントまたはクエリのみを返し、サブコレクションを返さない・クエリは常にドキュメント全体を返す・クエリはデフォルトでインデックス付きになり、クエリのパフォーマンスは、データセットではなく、結果セットのサイズに比例する
セキュリティルール 読み込みルールと書き込みルールはカスケード式に適応される ワイルドカードを使用する場合を除き、ルールがカスケード式に適応されることはない
スケーラビリティ スケーリングにはシャーディングが必要 スケーリングは自動的に行われる
課金 帯域幅とストレージにのみ課金され、課金レートは高くなる 主な課金対象は、データベースで実行されているオペレーション(読み取り、書き込み、削除)、帯域幅、ストレージ。帯域幅とストレージへの課金レートは低く設定されてる

FireStoreのデータモデル

FireStoreは、MySQLPostgreSQLなどのSQLデータベースと違い、「テーブル」や「行」はありません。代わりに、データはドキュメントに格納され、それがコレクションとしてまとめられています。

docuemnt.png

ドキュメント

ドキュメントは、JSONとよく似たデータ構造です。 ブール値、文字列、数値、タイムスタンプ、配列、マップなどのなどのデータ型を持つ値を、キーバリューによってデータを保存します。 例えば、ユーザーを表すドキュメントは次のようになります。

name: {
  firstName: '鈴木',
  lastName: '太郎',
},
sex: 'male',
birthDay: 847694648,
favoriteFoods: ['寿司', 'ラーメン', '焼き肉']

また、ドキュメントはスキーマレスであるため例えば同じユーザーを表すコレクションの中でも異なるデータ構造をもたせることができます。

name: '佐々木寿人',
birthDay: 847694648,
favoriteSongs: ['pretender', '紅蓮華', 'マリーゴールド']

ただし、あまりに自由なデータ構造をもたせるとアプリケーションで扱いにくいデータになってしまうので、スキーマを定義した上で使用するのが一般的です。スキーマレスな構造は、例えばレガシーなデータと互換性をもたせるために使われることがあります。

また、ドキュメントのデータサイズには制約があり、ドキュメント1剣あたりのサイズが1MBまでに制限されています。

ドキュメントはアプリケーションでそのまま扱えるように設計するのがポイントとなります。

コレクション

コレクションはドキュメントを格納するコンテナであり、すべてのドキュメントはコレクションの中に保存されます。 例えば、さきほどのドキュメントはUsersコレクションに格納されることになります。

コレクション内のドキュメントの名前は一意である必要があり、独自のキーをしていするかFirestoreで自動的にランダムなIDを振り分けることになります。

リファレンス

リファレンスは、ドキュメントが格納されているパスを表現するモデルで、データベースの場所によって一意に識別されます。 例えば、先程のUsersコレクションにアクセスするためには、次のようなレファレンスを作成します。

users/jkfjakdfjaffahi@a users/ahjioghja@gihjafu

また、リファレンスはそのままFirestoreにデータとして保存することができます。リファレンス型のデータの保存は、ドキュメント間の関係を表現する方法として利用されます。

サブコレクション

ドキュメントの階層構造を作るために、サブコレクションを利用することができます。 サブコレクションはドキュメントの中にさらにコレクションを持つという構造になっており、ルートコレクションから見るとコレクション/ドキュメント/コレクション/ドキュメントといった構造になります。 サブコレクションはドキュメントの親子関係、所有/被所有を表現するためにしようされ、例えばユーザー(Usres)記事(Articles)の関係は次のようになります。

users
  jkfjakdfjaffahi@a
    name: '鈴木太郎'
    sex: 'male',
      articles
        fjlkafakjfafflakju
          title: '記事1'
          body: 'ここに内容が入ります'
          published: false, 
          createdAt: 1560000000
  ahjioghja@gihjafu
    name: '佐々木寿人'
    sex: 'male',
      articles
        lkafhja;kfhahgi
          title: '記事2'
          body: 'あいうえお'
          published: true, 
          createdAt: 1460000000

リファレンスは次のように表します。

users/fjlkafakjfafflakju/articles/fjlkafakjfafflakju

なぜサブコレクションを利用するのか

このデータ構造を見て、こんなふうに思った方もいるのではないのでしょうか。 「Firestoreはそもそもデータ型としてリストやマップを備えているのだから、サブコレクションを利用しなくともそれらを利用すればよいのではないか」

つまり、次のようなデータ構造としても同じなのではないか、ということです

users
  jkfjakdfjaffahi@a
    name: '鈴木太郎'
    sex: 'male',
    articles: [
      { 
	id: fjlkafakjfafflakju
        title: '記事1'
        body: 'ここに内容が入ります'
        published: false, 
        createdAt: 1560000000
      }

確かに、この構造はよく見慣れたJSONの構造であり、必要なデータを一度のクエリで取得できるというメリットもあります。 しかし、以下の点から基本的に階層データはサブコレクションで保持すべきです。

ドキュメントのデータサイズには制限がある

前出のとおり、ドキュメントのデータサイズは1MBまでという制限があります。通常の利用には問題ないのですが、上記の例のようにドキュメントのリストやマップにネストした構造は、ユーザーの操作とともに数が増えていくようなデータを保持するには適していません。

クエリ上の観点

FireStoreでのドキュメントに対するクエリは、常にドキュメント全体を返します。 つまり、上記のデータ構造でユーザーデータを取得するとき、必要がないときでも常に記事のデータを取得しなければいけないため、クエリのサイズが大きくなり問題となります。

ドキュメントを取得するときに、通常その下の階層にあるサブコレクションは取得されません。サブコレクションは必要なときだけ取得すればよいことになります。

セキュリティルール上の観点

後述しますが、セキュリティルールを設計する際にもネストしたマップやリストを利用している場合問題が生じます。

セキュリティルールでは、for文や一時変数が利用できないため、リストの要素数がドキュメントごとに異なっていたり、ネストしたマップの型が統一されていない場合は安全なスキーマ検証ができなくなります。

さらに、データの秘匿に関しても問題になります。 例えば、ユーザのデータには公開してもよいデータ(名前、プロフィール)と他人には隠しておきたいが、本人は参照したいデータ(メールアドレス、住所)があるはずです。

以上のような問題は、サブコレクションによって解決されます。 サブコレクションはJSONツリー型でしかデータを保持できなかったRealTimeDatabaseの弱点を克服した構造ともいえるでしょう。

コレクショングループ

さらに、サブコレクションを利用する利点としてコレクショングループを利用することができるという点が上げられます。 コレクショングループは、同一のIDをもつサブコレクションを一つのコレクションとみなして扱うことができる機能です。 通常のクエリでは、UsersのサブコレクションであるArticlesを取得するには、users/{uid}/articlesとしてアクセスします。 ユーザーに紐づくすべての記事を取得するには単純ですが、 すべての記事を横断して取得するためにはユーザーごとの記事を取得する必要がありました。

しかし、コレクショングループを利用すれば、階層化されたサブコレクションを一度に取得することができます。

コレクショングループクエリを使用するためには、コレクショングループクエリをサポートするインデックスを作成する必要があります。 さらに、ウェブとモバイルSDKの場合は、コレクショングループクエリを許可するルールも作成する必要があります。

ドキュメントのデータ型

FireStoreのドキュメントには、以下のデータ型がサポートされています。

  • 配列(リスト)
  • ブール値
  • バイト
  • 日時
  • 浮動小数点数
  • 地理的座標
  • 整数
  • マップ
  • null
  • 参照(リファレンス)
  • テキスト文字列

FireStoreを使ってみる

Firebaseの概要についてここまで説明してきました。 ここからは、実際にFireStoreを使いながら進めていきます。

データベースを有効化する

Firebaseのプロジェクトを作成したら、左のナビゲーションバーからDatabaseを選択します。 スクリーンショット 20200503 18.33.54.png

データベースの作成をクリックして、テストモードを選択しましょう。

スクリーンショット 20200503 18.40.30.png

スクリーンショット 20200503 18.41.01.png

テストモードは、誰でもデータベースの読み取りや書き込みが行える状態であるため、決してテストモードのまま本番環境で使用してはいけません。

次に、データベースのロケーションを選択します。 ロケーションは、データを利用するユーザーとサービス近いほどレイテンシが小さくなります。 あなたが日本のユーザーをアプリケーションのターゲットにしているのなら、asia-northeast1(東京)asia-northeast1(大阪)を選択すれば無難でしょう。 スクリーンショット 20200503 18.49.07.png

データベースの作成が完了したら、次のような画面が表示されます。 スクリーンショット 20200503 18.51.40.png

データを追加する

それでは、早速データを追加しましょう。 ますはコレクションを開始します。ここでは、usersコレクションを作成します。 スクリーンショット 20200503 19.39.38.png

コレクションを作成したら、そのまま最初のドキュメントを追加しましょう。 ドキュメントのIDと、ドキュメントのフィールドを追加します。

スクリーンショット 20200503 19.42.51.png

ドキュメントのIDは、なにも入力しなけらばランダムなIDが自動で使用されます。 今回のようにusersコレクションを作成する場合には、 Firebase Authenticationを利用して作成したユーザーのuidを指定することが一般的です。 uidを使用したらドキュメントIDの一意性が確保されますし。ログインしているユーザーの情報を簡単に取得することができます。

ドキュメントのフィールドには、キーとタイプ、値を設定します。

キー タイプ
name string 鈴木太郎

ドキュメントの追加が完了したら、データが投入されていることが確認できます。

スクリーンショット 20200503 19.50.04.png

この画面から、さらにコレクションやドキュメントの追加、修正、削除などを行うことができます。

JavaScriptでアプリケーションからFirestoreを利用する

ダッシュボードからFireStoreを利用する方法はわかりましたが、おそらくこれはアプリケーションを利用する上で望んでいる方法ではないでしょう(すべてのアプリケーションの依頼をうけてあなたがデータベースを直接操作するようにしますか?)

Firebase JavaScript SDKを利用してアプリケーション上から操作できるようにしましょう。

開発環境の設定

まずは、アプリケーション上でFireStoreを使えるようにするための設定をします。

Firebaseライブラリの追加

<script>タグからFirebaseとFirestoreのライブラリをアプリケーションに追加します。

<script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-firestore.js"></script>

または、npmからパッケージをインストールします。

npm install firebase

npmを利用した場合には、インストールしたパッケージをimportしましょう。

import firebase from 'firebase/app'
import 'firebase/firestore'

Firestoreを初期化する

APIキーなどをセットして、Firebaseを初期化します。 Firestoreはfirebase.firestore()の名前空間から使用できます。

if (!firebase.apps.length) {
  firebase.initializeApp(
    {
      apiKey: process.env.VUE_APP_APIKEY,
      authDomain: process.env.VUE_APP_AUTHDOMAIN,
      databaseURL: process.env.VUE_APP_DATABASEURL,
      projectId: process.env.VUE_APP_PROJECTID,
      storageBucket: process.env.VUE_APP_STORAGEBUCKET,
      messagingSenderId: process.env.VUE_APP_MESSAGINGSENDERID,
      appId: process.env.VUE_APP_APPID,
      measurementId: process.env.VUE_APP_MEASUREMENTID
    }
  )
}

const db = firebase.firestore()

ドキュメントを追加する

これでFirestoreが使えるようになったので、早速基本のCRUD操作からやっていきます。 まずは、ダッシュボード上で行ったようにドキュメントを追加します。

IDを指定してドキュメントを追加

IDを指定してドキュメントを追加するには、ドキュメントのリファレンスを作成してからset()メソッドを使用します。set()メソッドは、ドキュメントIDがすでに存在する場合はドキュメントの更新を行い、存在しないドキュメントIDが渡された場合そのドキュメントIDで新規作成をします。

cosnt db = firebase.firestore()

// ログインしているユーザーの情報を取得します。
const user = firebase.auth().currentUser()

// ユーザーコレクションへのリファレンスを作成します。
const userRef = db.collection('user')

// ドキュメントIDにはログインユーザーのuidを指定します。
// setの引数にはJavaScriptのオブジェクトの形式でデータを渡します。
userRef.doc(user.uid).set({
  name: '鈴木太郎',
  age: 22,
  birthday: new Date('1996-11-11') // timestampe型にはDateオブジェクトを渡します。
  createdAt: db.FieldValue.serverTimestamp() // サーバーの時間をセットすることもできます。
})
.then(() => // 処理が成功したとき)
.catch(e => // エラーが発生したとき)

IDを自動で割り当ててドキュメントを追加

IDを自動で割り当ててドキュメントを追加するには、2つの方法があります。

  • set()
  • add()

なお、2つの方法は完全に等価であり、どちらか好みの方法を利用することができます。

set()を利用する

1つ目の方法は、IDを指定して追加する方法と同じく、set()を利用します。 単純に、doc()に何も渡さなければ、自動的にIDが割り当てられます。

cosnt db = firebase.firestore()

const userRef = db.collection('user')

// doc()の引数にはなにも渡しません
userRef.doc().set({
  name: '鈴木太郎',
  age: 22,
  birthday: new Date('1996-11-11') 
  createdAt: db.FieldValue.serverTimestamp() 
})
.then(() => // 処理が成功したとき)
.catch(e => // エラーが発生したとき)

add()を利用する

add()を利用しても同じようにドキュメントを作成できます。

cosnt db = firebase.firestore()

const userRef = db.collection('user')

// doc()の引数にはなにも渡しません
userRef.add({
  name: '鈴木太郎',
  age: 22,
  birthday: new Date('1996-11-11') 
  createdAt: db.FieldValue.serverTimestamp() 
})
.then(() => // 処理が成功したとき)
.catch(e => // エラーが発生したとき)

なお、ドキュメント作成時に存在しないコレクションが指定された場合には、そのコレクションも同時に作成します。

ドキュメントを更新する

ドキュメントを更新するには、以下の2つの方法があります。

  • update()
  • set()

この2つの方法は細部が異なるので見ていきましょう。

update()を利用する

ドキュメント全体を上書きせずに一部のフィールドを更新するには、update()メソッドを利用します。

cosnt db = firebase.firestore()

const userRef = db.collection('user')
// ログインしているユーザーの情報を取得します。
const user = firebase.auth().currentUser()

// ageフィールドのみを更新します

userRef.doc(user.uid).updaete({
  age: 24,
})
.then(() => // 処理が成功したとき)
.catch(e) = // エラーが発生したとき)

set()を利用する

set()メソッドは前述の通り、既に存在するドキュメントIDを指定した場合ドキュメントを更新します。 しかし、set()メソッドのデフォルトの動作に注意してください。set()のデフォルトの動作は引数で与えられた値でドキュメントを置き換えるため、もともと持っていたフィールドはすべて失われ新しいオブジェクトの情報だけ残ります。

つまり、update()を利用するのと同じ感覚で下記のように指定した場合異なる動作をするおのでは注意が必要です。

cosnt db = firebase.firestore()

const userRef = db.collection('user')
// ログインしているユーザーの情報を取得します。
const user = firebase.auth().currentUser()

// ageフィールドのみを更新しようとしましたが、もともと持っていたname、birthday、createdAtは失われてしまいます

userRef.doc(user.uid).set({
  age: 24,
})
.then(() => // 処理が成功したとき)
.catch(e) = // エラーが発生したとき)
// 予期した結果
{
  name: '鈴木太郎',
  age: 24,
  birthDay: timestampオブジェクト,
  createdAt: timestampオブジェクト
}

// 実際の結果
{
  age: 24
}

デフォルトの動作では、変更したくないフィールドも明示的に渡す必要があり、少々不便です。 そこで、一部のフィールドだけを更新したいときは、SetOptionsを第2引数に渡し、mergeパラメータにtrueを指定します。

cosnt db = firebase.firestore()

const userRef = db.collection('user')
// ログインしているユーザーの情報を取得します。
const user = firebase.auth().currentUser()

// ageフィールドのみを更新しようとしましたが、もともと持っていたname、birthday、createdAtは失われてしまいます

userRef.doc(user.uid).set({
  age: 24,
}, { merge: true })
.then(() => // 処理が成功したとき)
.catch(e) = // エラーが発生したとき)

ドキュメントを削除する

ドキュメントの削除には、delete()メソッドを利用します。

cosnt db = firebase.firestore()

const userRef = db.collection('user')
// ログインしているユーザーの情報を取得します。
const user = firebase.auth().currentUser()

userRef.doc(user.uid).delete()
  .then(() => // 処理が成功したとき)
  .catch(e) = // エラーが発生したとき)

単一のドキュメントを取得する

単一のドキュメントを取得するには、get()メソッドを利用します。

cosnt db = firebase.firestore()

const userRef = db.collection('user')
// ログインしているユーザーの情報を取得します。
const user = firebase.auth().currentUser()

const result = []
userRef.doc(user.uid).get()
  .then(doc => {
    if (doc.exists) {
        result.push({
          id: doc.id,
          ...doc.date() // doc.data()からデータのオブジェクトを取得できます。
        })
    } else {
      console.log('結果は空です')
    }
   })
  .catch(e => // エラーが発生したとき )

get()が成功したら、doc.exists()でドキュメントが空でないかチェックします。 ドキュメントが存在したのならば、doc.idでIDを、doc.data()からデータのオブジェクトを取得できます。

クエリを発行する

今週はここまで🤔

この記事をシェアする
Hatena

関連記事