結局Firebaseのdbでは何に気をつけるべきか?少しマニアックな内容も含め,わかりやすく説明する(realtime database)

  • 75
    いいね
  • 6
    コメント

Firebaseのrealtime databaseはいわゆるNoSQLであり,馴染みが薄い人が多いとともに,一般に流布している情報はふわっとしていてわかりづらい.
山に篭って修行していたらある程度知見が溜まってきたため,下山して共有する.

追記:
新しいdbが発表されたので雑感書きました.
Firebase RTDB + GCP datastore = Firestoreについて第一印象

tl;dr

割り切れ!
ユーザの見えるままに作れ!
それ以外の細末は下記参照

大原則その1:割り切り

これは特にRDB勢に気をつけていただきたい.
我々は少し気を抜くと,つい正規化したがり屋さんになってしまう.
(その意味では,むしろ開発経験が少ない人のほうが習得しやすいかもしれない)

Firebaseにおいて正規化はほぼ無駄と思ってもらっていい.
(理由は後述)
頑として非正規化を貫くべきだ(割り切り).

では,どう非正規化しろと言うのか.
巷では良く「ネストを浅くしろ」だの「非正規化しろ」だの言われるが,どのくらいどうすりゃいいんだよ!

大原則その2:ユーザの見えるままに設計する

これである.
だいたいまずこれで解決できる.

つまりまずビューありきなのだ(割り切り).
dbを先に設計してはならない.
(ビューを変更するときどうするか,は後述)

なぜそうしたほうがいいのか?

コードはシンプルなほうがいい

当然と言えば当然である.
スパゲッティ遊びしたい人は少数だろう.

実はビューありきでdb設計することによって,コードをシンプルにできるのだ.

勘のいいかたはおわかりだろうが,dbからread後の処理が最低限で済む.
ある意味時間と空間のトレードオフに近いものと思って良いだろう.

(知らない人には申し訳ないが)js界隈で話題になりがちなVuexやFlux, Reduxのような状態管理もほぼ不要となる(割り切り.詳細後述).

あるいはもっと深い理由として,こんなものがある.

システム全体としての効率化

実ユーザにおいては,圧倒的に書き込み数<<読み込み数である.
従って,効率化する(システム全体のパフォーマンスを上げる)ためには,なるべく読み込みをスピーディにするのが理に適っている

極端にも見える「ユーザの見えるままに」アプローチが確立された背景として,このようなことが挙げられるわけだ.
これらを踏まえると,むしろ従来の考え方を持ち込むのは害悪にしかならないことがわかる(割り切り).

「ユーザの見えるままに」を踏まえたところで,もう少し細かい原則を見ていこう.

原則

原則その1:dbはツリー状

json形式である.
というのは自明だが,key内にスラッシュが含まれていた場合は,その階層まで潜ると解釈される.

slashed.json
{"users/XXXXX/name": "yatima"}

とwriteすると

nested.json
{
  "users": {
    "XXXXX": {
      "name": "yatima"
    }
  }
}

として反映される.

原則その2:ノード以下まるごとread

課金やパフォーマンスとの関与が大きい話ではある(基本的にread時のダウンロードサイズに対して従量課金).

readでは,.ref()で指定したノードそれ以下の全てのデータを取得する.

fetch-good.js
const db = firebase.database()
const hogeRef = db.ref('hoge')
hogeRef.on(...省略...)

とすると,/hoge/以下のみを取得する.

fetch-yabai.js
const db = firebase.database()
const dbRef = db.ref()
dbRef.child('hoge').on(...省略...)

とすると全部ごそっと取得することになり,悲しみが生まれる.

ただしクエリを含めた場合は,それに該当する分のみ取得される.

原則その3:連番は使わない

例えばカウンタなどで,ほぼ同時にwriteが行われたとすると,残念なことになる.
(一人目が書き込み終わる前に二人目が読み込んでしまうと,二人ともカウンタを1増やしたつもりが最終的に1しか増えない)
.push()を使うと,一意なIDをkeyとして書き込みできる.

push.js
const db = firebase.database()
const hogeRef = db.ref('hoge')
hogeRef.push('fuga')

と書き込むと

db.json
{
  "hoge": {
    "<XXX-unique-id-XXX>": "fuga"
  }
}

となる.

逆に言えば,.push()を使わない場合は変に上書きされても問題ない前提で開発すべきである.

原則その3.1:配列を使わない

その3と似た理由で,配列(Array)は推奨されない.
連想配列(Object)に自動で変換されるが,その際にkeyが連番で割り当てられてしまうからである.

原則その4:一貫性のある同時書き込み

単に同時書き込みするだけで一貫性が保たれる仕様である.

multi-path-write.js
const db = firebase.database()
const dbRef = db.ref()

const newPost = {
  hoge: "fuga"
}
let updates = {}
updates['foo'] = newPost
updates['bar'] = newPost
dbRef.update(updates)

とすると

db.json
{
  "foo": {
    "hoge": "fuga"
  },
  "bar": {
    "hoge": "fuga"
  }
}

となる.
サーバ側でvalidateすることも可能だが,不要だとは思われる.一応示しておく;

consistency.rules.json
{
  "test": {
    ".write": "true",
    "foo": {
      ".validate": "true"
    },
    "bar": {
      ".validate": "newData.val() == newData.parent().child('foo').val()"
    }
  }
}

間違っても.transaction()など使ってはならない
(一ノードに対してしか使えない=rootに近いノードを指定するハメになる,そしてトランザクションはreadが発生する ⇒ 諭吉ジャブジャブ,性能にも影響)

と,比較的重要な原則についても紹介してきたが,いずれにせよあくまで大原則に沿った話である.
大原則が通用しない場合はどうすべきだろうか.

大原則が通用しない時

ビューが強く動的に生成される

ここでの動的とは,静的ファイルが準備されていないという程度の意味ではない.
検索のような,ユーザの入力を元にビューを変化させたい,そういったニュアンスである.

検索

検索には,Algoliaという外部サービスを利用する(割り切り).
Firebase公式より連携サンプルも提示されている.
ただしどうやらこのサービスはORやワイルドカードなどは使えないように見える.

他にもFlashlightというElasticSearchとの連携があったりなかったりする.
ワイルドカードは使えるとのことだが,公開されているサンプルはうまく動かない.

フィルタ

フィルタに関しては内部で解決可能と思われる.
基本的にはフィルタのパターン数に応じ対策も変わる.
ざっくり言って,

フィルタのパターン数が多いならば,インデックスを貼る.
必要があればクライアント側で集合演算も行う.
(ANDのみならインデックス貼りまくるだけで可能だが,ORがあると厳しい)
JavaScriptならlodashなど使うと良い.

フィルタのパターン数がそこまで多くないならば,クエリを駆使する.
クエリについては,公式で良い解説動画がある(日本語字幕付き).
Common SQL Queries converted for the Firebase Database - The Firebase Database For SQL Developers #4
hackyな手法にも見えるが,性能に大きな影響を与えないためのFirebaseの流儀である.

詳細は後述しよう.

そもそもビューがない

もしFunctionsや他サーバなどから機械的にreadする必要がある場合は,「readする時使いやすいように」で良いと思われる.
なるべく事前に準備しておくという意味で,「ユーザの見えるままに」とは共通する.

機械によるreadもなく,保存自体が主な目的の場合は(ログなど?),まぁ好きにしたらいい.

レシピ(加筆中)

カウンタ

IDを数える.

ページネーション

クエリで良い.

フィルタ

検索

よく言われるアレ(気付き次第追記)

「ビューとdbを密結合させたらビューを変更する時に不便」

多くは見た目の変更で,扱うデータ自体が変更されることは少ないだろう.
変更されたところで,構造を修正するバッチを回せば済む話だ(割り切り).

「非正規化が理想なのはわかるけど,でも現実的には正規化したほうがいいよね」

これもなるべくなら避けるべきだ(割り切り).
というより,上述した特殊な場合以外は現実的に問題ないはずである.

再度になるが,これまで培ってきた感覚で「正規化したほうがよさそう」は避けたほうが良い.
コードが複雑になり,パフォーマンスにも影響が出かねない.

「平坦化しろ」

自分が検証した限りでは,単にdbのネストが深いからといって,パフォーマンスが落ちることはなさそうである.
そのため,「平坦化する」ということ自体をメインの目標に掲げるのは本質的でない.

目指すはあくまで「ユーザの見えるままに」とすべきである.

「ネストの度に検索がかかるのでは」と思ったかたは鋭い.
RDBをわかっていらっしゃる.

realtime databaseでは,jsonのkeyに対してはインデックスが貼られるのだ.
valueに対しても,インデックスを指定して貼ることが可能(性能に影響するとアラート出るので,それからで遅くない).

やはりあくまで中心は「ユーザの見えるままに」である.

「クエリが貧弱」

おわかりのかたもいると思うが,そもそも基本的にクエリは最低限でよいのだ.
dbをreadした時点で,ビューに即した形になっているのだから.

「値段高い」

のちほど

「PHPと組み合わせたい」

なるべく避けるべき(割り切り).

既存の技術を使わないのはもったいない,あるいは不安と思うかもしれない.
しかしFirebaseはシンプルなシステム構成にしてこそ真価が発揮される.
連携間の相性に悩まされることがなくなるわけだ.
あるいはデータ変換やAPIなど余計なことに煩わされなくなる
当記事に記すような考え方が身についてしまえば,学習コストも低い

ぜひ0から始める気持ちで臨んでほしい(割り切り).
もちろん,RubyやPythonだろうが同様である.

よく言われないアレ

tips集

削除時にはnullをwriteする

.set().update().push()でundefinedは送れない.
.remove()もあるが,複数箇所の削除はできないのでいまいち.

一度のみのreadには.once()

上記の通りである.

クエリを使うならchild_added

read時にはvalueを使う(例えば.on('value', ...)のように)と手軽のように思えるが,実際はchild_addedのほうが良い.
後者ならば,順番の整った状態で取得できるためである.
前者だとクライアント側で並び替えする必要がある.

child_changedなどもいちいち書かなければならない?
例えばVuefireのような,フレームワークと紐付けしてくれる便利なライブラリがある.

正規化は無駄

先に謝っておく.無駄は言い過ぎた.
しかし総じてメリットが上回ることは多くないと思われる.

慣れ以外でのメリットは,高々,容量を節約できるくらいと思われる.
正規化したところでクエリが貧弱なため,まともに使うには苦労する.
この後に示す,グローバル的な状態管理も欲しくなってくる.
コードが複雑化するのは代償として大きい.

状態管理は不要

ビューに即したデータ構造を持たせるのだから,基本的にはVuexやFlux, Reduxのような状態管理(いろいろな場面でデータを共有する仕組み)は不要.
せいぜいユーザ情報程度だろう.

firebase自体もローカルでデータ保持することを考えると,無駄が多い.

ベストなjsフレームワーク

dbの話なのにjsのフレームワークまで踏み込むのもアレだが,ビューとも密接な関係にあるため触れておく.

公式ではAngularが話題に挙がりやすいが,上記の状態管理機能などももりもり含んでおり,重厚長大過ぎる(割り切り).

Vue.jsが総合的に判断してベストだろう.
詳細は省くが,メリットは以下の通り.
・とっつきやすさ
・単一ファイルコンポーネント*.vue(html, css, jsを一ファイルにまとめられる)
・日本語ドキュメントが充実
・それなりの知名度,将来性
・必要に駆られた際の拡張性
元Googlerが中心人物のため,Google教徒の我々にとっても安心である.

一時期LAMPスタックという表現に則ってFIREスタックという表現が提唱されたが,自分は更に推し進めてFiVeスタックを提唱したい.

FiVeスタック

Firebase + Vue.jsである.

元ネタのFIREスタックはFirebase + Interface + Reactorであるが,

Interfaceとは,ユーザとのインターフェースである.
jsのフレームワークや,ネイティブアプリを含むこともある.
上述の通りVue.jsを推す.

Reactorとは,サーバサイドの処理である.
現在はFirebase内にも用意されているため,略語としてFirebaseに包含できてしまう.

つまるところ,FiVeスタックである.

ちなみに:vue-material

Vue.jsでmaterial designを使う場合,vue-materialが便利である.
神楽坂やちまはvue-materialで唯一のロゴ掲載スポンサーです(ステマ)

…と,だいぶ話も逸れてしまった.
閑話休題.
これまでの内容を踏まえつつ,どのようなサービスに使いやすいか使いにくいかを解説したい.

サービスの向き不向き

もちろん,Firebaseも万能ではない.

全体的に「細かい所は別に気にしなくてよくない?」という思想に基づいている(割り切り).
厳密性をウリにするような,企業や研究向けサービスの一部には向かないだろう.
クエリが貧弱なので高度な分析なども苦手で,そういった意味でも使いにくい.

あるいは初出ではあるが,レイテンシもめちゃくちゃ低いというわけではない.
(WebSocketなのでRESTと比べて速いのは確か)
格闘ゲームやFirst Person Shootingのような,わずかな遅延が致命的となるゲームも向かない

しかし,これらに含まれない大半のサービスでは,Firebaseの特徴がいずれかの部分でメリットとなるはずだ.
(但し多くの人にとっては多少の学習コストが要るだろうが)
本質的に,MySQLを置き換えるだけの可能性は十分に秘めている.
(だからFiVeスタックを提唱したのだ)

そして兎にも角にも,まず気をつけるべきは
「割り切り」
「ユーザの見えるままに」
である.

「dbはツリー状」
「ノード以下まるごとread」
「連番や配列は使わない」
「一貫性のある同時書き込み」
を意識しつつ,まずは
「割り切り」
「ユーザの見えるままに」
である.

しかし大事な話を忘れていた.
価格を含めた上で,そもそもFirebaseは採用に値するのだろうか?

「でもお高いんでしょう?」「やっぱりMySQLのほうが…」

ここらの話はGDG DevFest Tokyo 2017で登壇して話すので,ぜひいらして欲しい.
大半のウェブサービス/アプリは,Firebaseなら簡単で安いですよ11:30- @会議室1
チケット登録

当記事はあくまでrealtime databaseのベストプラクティスとして踏み込んだ記事であるため,このイベントではより広い視点で解説するつもりである.

他にご質問,ご意見などあればぜひお気軽に.
厳しいマサカリもお待ちしております.

todo

レシピ埋めたい
そのうちruleについても加筆したい

参考記事

Firebase: 5 way-too-common misconceptions
 (記事最後のフォームより,ベストプラクティスのチェックリストがPDFで入手できる)
The Firebase Database For SQL Developers(公式youtube.非常におすすめ,日本語字幕あり)
Firebase Data Modeling
Best Practices for Firebase Realtime Database Development
Firebase Pros and Cons

 

 

└(・∀・)┘ヤリィ!