初めに
業務でFirestoreを使用する機会があったので学習した内容をアウトプットする。
この記事は、Firestoreの公式の「Cloud Firestore について理解する」という章に関してのアウトプットである。
Firestoreとは
クラウドで管理されるNoSQLであり、iOS、Android、WebがSDKを使用して直接アクセスすることができる。
Node.js、JavaやREST・RPC APIなどに用いられる。
また、データが更新するたびにテーブル全体の情報を取得するのではなく、リアルタイムリスナーを追加する。
リアルタイムリスナー・・・クライアントが指定しているデータが更新された場合にスナップショットで通知されて新しい変更のみを取得することができる。
構造
Firestoreの構造は値をマッピングするフィールドを含むドキュメントにデータが保存される。
また、ドキュメントはコレクションに格納されている。
ドキュメントはJSONのような形式をしており、複雑な階層データはドキュメント内のサブコレクションを使用して整理することができる。
上図の参照: FIrebase公式
データモデル
Cloud Firestoreは、ドキュメント指向データベースであり、各ドキュメントには一連のキーとバリューが存在する。
コレクションとドキュメントはFirestoreが暗黙的に生成することも可能である。
また、スキーマレスのデータベースであり、各ドキュメントに保存するデータ型を自由に決めることができる。
構造
ネストしないドキュメントの構造
通常のドキュメントの構造である。
firstname : "taro"
lastname : "yamada"
age : 20
ネストするドキュメントの構造
例として、同じ名前
であるfirstname
とlastname
を1つのドキュメントにネストしてまとめてみる。
name :
first : "taro"
last : "yamada"
age : 20
コレクションを含めた構造
コレクションを含めた構造だとコレクションに複数のドキュメントが格納されている。
例として、users
コレクションに複数のドキュメントが格納されている構造を記載する。
users: {
document1
document2
}
RDBと異なる点
RDBだとテーブルにレコードが複数あり、各カラムには厳密な方が用意されているが、Firesore(NoSQL)ではその限りではない。
例えば、以下のような状況もあり得る。
users(collection)
field1
first : "Ada"
last : "Lovelace"
age : 22
field2
first : "Alan"
last : "Turing"
born : 1912
上記だと、片方のfieldにはage
というキーが存在しているがもう片方のfieldにはage
キーは存在せずborn
というキーが存在している。
このように、存在するキーがその他のfieldと異なっていても問題ない。
その点はRDBに比べて自由度が高いと言えるであろう。
ただし、複数のドキュメントで同じフィールドとデータ型を使用する方がクエリを簡単に実行するため、そちらの方が推奨されている。
また、Firestoreだとデータの構造が必ずしも一定でないためクライアントが、RDBに比べて取得する値のチェックをより慎重に行う必要がある。
コレクション内のドキュメントの名前は一意である。
結合
Firestoreでは、RDBのテーブル結合のような機能は使用しない。
こちらの動画によると、RDBのテーブル結合のようにテーブルの複数の情報を取得したい場合は以下の2点の解決策がある。
①取得したいテーブルの数だけだけSQLを発行する。
②データを複数の箇所に保存する。
参照した動画によると②の方法を推奨しているため、簡単に対応内容を記載する。
例
上図の場合は顧客と注文の関係性が1対多になっている。
その場合は、以下のように、注文に顧客の情報を格納することで解決する。
注文に顧客の情報を格納することによって1回のリクエストで複数のテーブルの情報を取得することが可能になる。
しかし、上記の対策には問題があり、顧客の情報が変更された場合に注文の情報を複数書き換える必要がある。(今回の例だと顧客の情報が変更されることは考えにくいが)
そのため、上記対策ではデータの書き換えが多く発生することが予想される。
ただし、通常のデータベースの扱いであれば書き込みよりデータの読み込みの機会の方が多いと考えられる。
Firestoreだと、テーブルの結合がない分データの読み込みが高速になる。
よって、上記対策によるデータの書き換えをによるコストを考慮しつつ、それでもデータの読み込みを高速に行いたい場合にはRDBよりも機能する可能性がある。
ルール
- コレクションは、ドキュメントのみを格納することができ、文字列やバイナリ・ブロブなどは格納することはできない。
- ドキュメントのサイズは1M未満であり、それより多い場合は分割する必要がある。
- ドキュメントにドキュメントを格納することはできない。
- Firestoreはツリー構造であり、最上階はコレクションである必要がある。
- データを絞り込む際は「コレクション → ドキュメント → コレクション → ドキュメント」のように順番に絞り込む必要がある。しかし、これだと冗長になるので、パスを指定して絞り込むことも可能である。パスの中でもコレクション、ドキュメントの順に指定する必要がある。「collection1/document1/collection2/document2」など。
サブコレクション
コレクションにはサブコクションを作成することができる。
サブコレクションを用いることで階層的なデータ構造を再現することが可能である。
詳細に関してはこちらの方に記載してある。
コレクションを削除した場合にはサブコレクションは削除されていないので注意が必要である。
データ型
こちらを参照すると分かりやすい。
スケーラビリティ
異なる地域のデータセンターにデータが保存されており、スケーラブルである。
また、複数のデータセンターに保存されているので、1つのデータセンタが何らかの場合に故障した場合でも別の保存しているデータを使用することが可能である。
また、自動的にスーケーリングしてくれる。
データベースへのアクセス方法
データベースにアクセスすには、直接HTTPやgRPCを使用してアクセスする方法もあるが、SDKを使用してアクセスすることも可能である。
SDK
クライアントとデータベースの間にサーバーを別途用意するような場合はサーバークライアントライブラリを使用する。
サーバークライアントライブラリは、C#、Node、Goなどがサポートされている。
FirebaseAdminSDK
FirebaseAdminSDKの特徴は以下である。
- データベースに対してフルアクセス権限を持つ。
- Firebase 認証トークンを生成して検証する
詳細はこちらに記載してある。
筆者だとGoを用いてFirestoreを使用する予定なのでGo × FirebaseのドキュメントやGo × Firestoreのドキュメントのドキュメントが参考になりそう。
インデックス
Firestoreでは、全てのクエリにインデックスを用いているため高いクエリパフォーマンスを実現できる。
Firestoreでは、Single-field indexesとComposite indexesの2種類のインデックスが存在する。
Single-field indexes
Single-field indexesは、フィールドの値とデータベース内のドキュメント内の場所を格納している。
また、昇順・降順の2つのモードを格納している。
基本的にインデックスは自動で生成される。
single-field index exemptionを使用することでインデックスを免除することも可能である。
※ exemptionは「免除」などと訳される。
Composite indexes
Composite indexesでは、Single-field indexesのように自動的にインデックスが生成されないので自ら作成する必要がある。
インデックスモードとクエリスコープ
Single-field indexes、Composite indexesのどちらもインデックスモードとクエリスコープを選択する必要がある。
インデックスモード
昇順・降順・配列のいずれかを選択する
クエリスコープ
各インデックスは、コレクションまたはコレクショングループのいずれかにスコープされている。
Firestoreでは、デフォルトでコレクションスコープを作成する。
また、コレクショングループには同じコレクションIDを持つコレクションが含まれている。
クエリ
インデックスを使用したクエリを記載している。
Single-field indexes
単一のフィールドを指定
var collect_variable = db.collection(コレクション名)
// 「=」の箇所は「 <, <=, ==, >=, >, !=, in」などに臨機応変に変更する。
const stateQuery = collect_variable.where("field名", "==", 取得したい値)
複数のフィールドを指定
collect_variable.where("field名", 'in', ["取得したい値1", "取得したい値2", "取得したい値3"])
collect_variable.where("field名", "==", "取得したい値").where("field名", "==", "取得したい値")
Composite indexes
< 、 <= 、 > 、または>=
を使用した複合クエリを発行する場合や別のフィールドを並び替える場合はComposite indexesを使用する。
Order句を使用する
collect_variable.where("field名1", "==", "取得したい値1").orderBy("field名2", "asc")
collect_variable.where("field名1", "==", "取得したい値1").where("field名2", "<", 値2)
collect_variable.where("field名1", "==", "取得したい値1").where("field名2", ">", 値2)
上記だと field名1のascもしくはdesc
、field名2のasc
のComposite indexesが必要である。
デフォルトでは、不等式句を使用する場合は昇順のソート順を適応する。
field名2
を降順で指定したい場合は、新たにComposite indexesを作成する必要がある。
コレクショングループ
コレクション全体を取得したい場合は以下の構文を使用する。
db.collectionGroup("取得したいコレクション").get()
コレクション全体をインデックスを使用して、取得したい場合は以下の構文で可能である。
var variable = db.collectionGroup("取得したいコレクション")
生成されるインデックス
生成されるインデックスはこちらに詳細に記載されている。
価格
インデックスはストレージのコストに影響する。
実際にコストがかかるような実運用する場合は注意が必要である。
複雑なデータを管理する場合
複雑なデータを管理する場合に以下の3点のデータの格納方法がある。
①ドキュメント内のデータをネストする
②サブコレクションを使用する
③ルートレベルのコレクションを使用する
データ格納方法 | メリット | デメリット | 向いている例 |
---|---|---|---|
データをネスト | ドキュメント内のデータが固定されている場合に適合している | 変化するデータだと、ドキュメントの量が多くなりすぎたりするので時間と主に変化するデータには合わない | 苗字と名前などの組み合わせ |
サブコレクション | 時間経過などでデータが変化する場合に合理的 | サブコレクション自体の削除が容易ではない | チャットアプリのメッセージなど(メッセージサブコレクションを作成する) |
ルートレベルのコレクション | 多対多の関係に適している。 | 扱うテーブルの量が増えた際にルートレベルのコレクションが増えすぎる | チャットルームの「ユーザー」と「ルーム」など |