LoginSignup
22
18

More than 3 years have passed since last update.

FireStoreの基本と操作 - データのCRUD操作とクエリ

Posted at

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()からデータのオブジェクトを取得できます。

クエリを発行する

単一のドキュメントに対するCRUD操作を見てきました。
しかし、一般的なアプリケーションでは複数のデータを条件によって取得する欲求があるはずです。Firestoreがどのようばクエリを発行できるか見ていきましょう。

単純なクエリ

次の例は、すべての記事を返します。

cosnt db = firebase.firestore()
// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef.get()
  .then(querySnapshot => {
    if (querySnapshot.empty) { // querySnapshot.emptyがtrueの場合コレクションにデータが存在しません。
        console.log('結果は空です')
    } else {
      // querySnapshotをループしてデータを取り出します。
      querySnapshot.forEach(doc => {
         // 単一のドキュメントの操作と同じです。
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

コレクションの参照にget()メソッドを利用して、すべてのコレクションを取得することができます。
また、一般的なNoSQL系データベースと異なりFirestoreのクエリ結果はすべて強い整合性をもつことが特徴です。サーバーからドキュメントを取得する場合は
常に最新のデータにアクセスすることが保証されています。

フィルタを利用する

Firestoreでは、SQLデータベースのようにwhere()メソッドを利用することでクエリをフィルタリングすることができます。
where()メソッドは、3つの引数を受け取り、フィルタリングするフィールド、比較演算、値の順に受けれ入れます。
比較演算子には、以下の8つが利用できます。

  • =
  • <
  • <=
  • >
  • >=
  • in
  • array-contains
  • array-contains-any
=(等価演算子)

次の例は、ログイン中のユーザーの記事を取得します。

cosnt db = firebase.firestore()

// ログインしているユーザーの情報を取得します。
const user = firebase.auth().currentUser()
// ユーザードキュメントへの参照を取得
const userRef = db.collection('user').doc(user.uid)

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  // auhtorフィールドは参照型です
  .where('auhtor', '==', userRef)
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

クエリに.where('auhtor', '==', userRef)が追加されています。これが基本的なwhere()メソッドの使用方法です。

< <= > >=(比較演算子)

比較演算子も同じように利用できます。
次の例は、2020年4月以降の記事を取得します。

cosnt db = firebase.firestore()

// 起点となる日付を作成
// firestoreの日付型はDateオブジェクトで比較できます。
const date = new Date('2020-04')

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  // createdAtフィールドは日付型です
  .where('createdAt', '>=', date)
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )
inクエリ

inクエリはフィールドがいくつかの値のいずれかに等しいドキュメントを取得します。
inクエリは、Firestoreで単純なORクエリを実行するのに適した方法です。

次の例は、記事のタイトルが「Denoとはなにか - 実際につかってみる」「FireBase①」「JavaScript ES2015」の記事を取得します。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  // 配列で値を渡します
  .where('title', 'in', ['Denoとはなにか - 実際につかってみる', 'FireBase①', 'JavaScript ES2015'])
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

なお、inクエリに渡せる値は10個までという制約があります。

array-contains(配列メンバーシップ)

array_containsは配列型のフィードに対して使用します。
フィールドの配列に値が含まれていた場合、そのドキュメントを返します。
次の例は、JavaScriptというタグが使用されている記事を取得します。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .where('tags', 'array-contains', 'JavaScript')
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

クエリ対象の値が配列内に複数存在する場合でも、ドキュメントは結果に 1 回だけ含まれます。

array-contains-any(配列メンバーシップ)

array-contains-anyは、配列型に対するinクエリです。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .where('tags', 'array-contains-any', ['JavaScript', 'PHP', 'Firebase'])
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

inクエリと同様、渡せる値は10までの制約があります。

複合クエリ

1回のクエリの中で、複数のwhere()メソッドを呼び出して作成することができます。複合クエリはAND条件として扱われます。

等価演算子=に対する複合クエリ

等価演算子==に対する複合クエリには制限がなく、複数回フィルタをかけることができます。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .where('auhtor', '==', userRef)
  .where('createdAt', '==', new Date())
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )
比較演算子に対する複合クエリ

比較演算子に対して複合クエリを使用する場合、1つのフィールドに対するクエリは有効です。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .where('createdAt', '>=', new Date('2019-04'))
  .where('createdAt', '<', new Date('2020-04'))
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

しかし、複数のフィールドに対して同時に比較演算子を使用することはできません。次のようなクエリはエラーになります。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  // 複数のフィールドに対する比較演算子はエラー!
  .where('createdAt', '>=', new Date('2019-04'))
  .where('rating', '<', 5)
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )
等価演算子と比較演算子、配列メンバーシップを同時に利用する

等価演算子と比較演算子、配列メンバーシップを同時に利用するクエリでは、**複合インデックスを作成する必要があります。
例えば、複合インデックスを作成していない状態で次のようなクエリを発行しようとしてみます。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  // 等価演算子と比較演算子を同時に利用する
  .where('createdAt', '>=', new Date('2019-04'))
  .where('published', '==', true)
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

次のようなエラーが発生してしまいました。

スクリーンショット 20200524 16.39.11.png

このクエリにはインデックスが必要ですという旨のエラーです。
メッセージに示されたURLをクリックすると、コンソールへ移動して自動的に複合インデックスを作成してくれます。

スクリーンショット 20200524 16.42.56.png

inクエリ、配列メンバーシップ

inarray-containsarray-contains-anyは、複合クエリの中で一度だけ使用することができます。

クエリのソート

orderBy()メソッドを使用すると、クエリ結果を並び替えることができます。1回のクエリで複数のフィールドに対してソートをすることができます。
次の例では、作成日の降順、評価の昇順で並び替えます。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .order('createdAt', 'desc')
  // ソート順を指定しなかった場合、昇順になります。
  .order('rating')
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

なお、 orderBy() メソッドは、指定したフィールドの有無によるフィルタも行います。 指定したフィールドがないドキュメントは結果セットには含まれません。

orderBy()メソッドはwhere()メソッドと組み合わせて使用することができますが、比較演算子を利用する場合には最初の並べ替えは同じフィールドである必要があります。
次のクエリはエラーになります。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .where('createdAt', '>=', new Date('2019-04'))
  // 比較演算子と異なるフィールドでソートしようとするとエラー
  .order('rating')
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

さらに、等価演算子を利用して異なるフィールドでソートする際には複合インデックスを作成する必要があります。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .where('published', '==', true)
  // 複合インデックスの作成が必要
  .order('createdAt', 'desc')
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

データの取得数の制限

limit()メソッドを利用すると、データを取得した数だけ取得します。
次の例では、最新の記事上位10件に限って取得をします。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
articleRef
  .order('createdAt', 'desc')
  // 10件だけ取得
  .limit(10)
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

ページネーション

Firestoreのクエリを用いてページネーションを行ってみましょう。
limit句は先程紹介しましたが、offset句はサポートしておりません。その代わりには、startAfter()を利用してクエリの開始点を指定することでページネーションを実現します。

startAfter()には、パラメータドキュメントを渡すことができます。つまり、前回実施したクエリの最後のドキュメントを指定すれば、次のページを取得することができます。

cosnt db = firebase.firestore()

// 記事一覧への参照を作成
const articleRef = db.collection('articles')

const result = []
const limit = 10
// 最後のドキュメントを保持しておきます。
let lastDoc
// すべてのドキュメントを取得したかの判定に使用します。
let isFinish = false

articleRef
  .order('createdAt', 'desc')
  .limit(limit)
  .startAfter(lastDoc)
  .get()
  .then(querySnapshot => {
    if (querySnapshot.empty) {
        // 取得したコレクションが空だったらすべてのドキュメントを取得したと判定
        isFinish = true
    } else {
      if (querySnapshot.size < limit) {
          // 取得したコレクションの数がlimitよりも少なければこれ以上データはない
          isFinish = true
      }
      // 最後のドキュメントを取得
      lastdoc = querySnapshot.docs[querySnapshot.docs.length - 1]
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

2ページ以降も、最初のページと同じ条件のクエリを発行する必要があります。
また、明確にページ数を指定するタイプのページネーションは推奨されていません。(3ページ目を取得しようとしても、2ページ目の終わりがわからない)
無限スクロールによるページネーションの実装が推奨されています。

リアルタイムリスナー

Firestoreの大きな特徴の一つとして、リアルタイムリスナーがあります。リアルタイムリスナーは、クライアント側でFirestoreの最新の状態を監視し、変化があった場合には直ちに状態を同期することができます。
リアルタイムリスナーを利用するにはget()メソッドの代わりにonSnapshot()メソッドを利用します。

cosnt db = firebase.firestore()
const articleRef = db.collection('articles')

const result = []
articleRef
  .getonSnapshot()
  .then(querySnapshot => {
    if (querySnapshot.empty) { 
        console.log('結果は空です')
    } else {
      querySnapshot.forEach(doc => {
        result.push({ id: doc.id, ...doc.data() })
      })
    }
   })
  .catch(e => // エラーが発生したとき )

リアルタイムリスナーは、例えばチャットのような機能も簡単に実装することができます。

また、ドキュメントがどのような変更がされたか確認することもできます。

cosnt db = firebase.firestore()
const articleRef = db.collection('articles')

const result = []
articleRef
  .getonSnapshot()
  .then(querySnapshot => {
    snapshot.docChanges().forEach(change {
      if (change.type === "added") {
        console.log('追加されたドキュメント', change.doc.data());
      }
      if (change.type === "modified") {
        console.log('変更されたドキュメント', change.doc.data());
      }
      if (change.type === "removed") {
        console.log('削除されたドキュメント', change.doc.data());
      }
     })
  }

リアルタイムリスナーはユーザー体験を向上させますが、単純なクエリのほうが適している場合もあります。
例えば、ブログなどで記事を見ている最中に(今この瞬間ですね)突然本文の内容が変わったり削除されたりすることを好ましいと思う人は少ないでしょう。

また、先程のページネーションと組み合わせたりするときも注意が必要です。ページ送りをしている最中にデータの並び順が変わった場合、再度同じドキュメント取得してしまったりなどページ付がおかしくなったりすることがあります。

さらに、データが頻繁に更新されるような場合、データが次々と追加されたり入れ替わるさまを眺めるのは楽しいかもしれませんが、バッテリーや通信量の面でユーザーからは不評を得るかもしれません。

22
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
18