大規模でも開発保守を極めて容易にする;データベースすらコンポーネント化

(この記事はVue.jsのアドベントカレンダーですが,Vue.jsが最適というだけで,他のjsフレームワークやAndroid, iOSでも応用可能な話です)

tlllll;dr

実はdbよりviewを先に作るとある意味非常に楽になる
(さすがにこれだけだと意味不明すぎると思うのでぜひ記事を.意味ないかと思って今回のtl;drはなくしていたのですが,やっぱ長過ぎるので付けときます)

導入

はじめましてのかたははじめまして.
はじめましてじゃないかたもはじめまして.
株式会社みんコレCTO 神楽坂やちまと申します.

突然ですが.
みなさん,複雑怪奇なソースコードは嫌ですよね?

ソースコードが複雑になる大きな要因として,「ユーザ画面とデータベースの構造の不一致」というものがあります.

「いやいやそんなの,柔軟性を考えれば当然だ」あるいは「ユーザ画面に合わせてデータベースを複製するなんて無駄が多すぎる」と思われるかもしれません.
確かに,これらを切り離すことでもたらされていた柔軟性などは大きなメリットでした.
しかし同時に,切り離された両者をつなげる処理が複雑になり,開発コストの重荷となっていたのも確かかと思います.

ここでもし,現実的なレベルでユーザ画面とデータベースの構造を一致させることが実現できたら,色々とシンプルに済みそうではありませんか?
.vueのファイル形式をご存知のかた,データベースもその延長上でいけるイメージですよ!)

FiVeスタック(FirebaseとVue.js)を使うことによって,ユーザ画面とデータベースの構造を一致させることが容易に実現できる1のです.
(AndroidやiOSでもFirebaseを利用すれば可能です.jsではVue.jsが相性良)
 
※ この記事で,FirebaseとはFirebaseのデータベースを指します.
Firebaseとは何か,Firebaseのデータベースはどんなものか,については後日別記事を執筆予定です.→しました.まだ間に合う!着々と進化しているFirebaseをまとめてみるよ
こんな記事なら書いてます;結局Firebaseのdbでは何に気をつけるべきか?少しマニアックな内容も含め,わかりやすく説明する(realtime database)

データベースについては一言で言っておくと,クライアントから直接書き込める,リアルタイムでオフライン対応なNoSQLデータベースです.
また,Firebaseには現在2種類のデータベースがあります.
違いは過去記事など参照.

背景事情

今回の話に至ったのには,いくつかの文脈があります.

Firebaseでは,実はSPAは容易(もちろんネイティブアプリも)

特に最近話題になりやすい,SPAという言葉があります(Single Page Application;ウェブ上で,AndroidやiOSのようなネイティブアプリに近い使い勝手を提供する).
一般には開発コストが多めにかかる,と言われやすいかと思います.
当然ですね,従来のサーバ側に加え,クライアント側で実装する処理も多くなるためです.

そもそも従来はサーバ側での処理がメインだったとは言え,むしろクライアントでのjs処理もそれなりにありました.
ネイティブアプリであれば尚更です.

それが,Firebaseを使えばシンプルにクライアント側で済ませられるのです.
Firebaseのデータベースでは,直接クライアントが読み書きできるので,そもそも基本的に処理はクライアント側で行います.

つまり,まず開発コスト的には,Firebaseの積極的な導入検討に値することになります.

データベースが手軽に使えるように

従来データベースと言えば,大掛かりなものでした.
データベースサーバを立て,他サーバとの通信を構築し,使うときや設計を変える時にはスキーマを書き,成長すれば増強を考えねばなりませんでした.

Firebaseではこれらは不要です.
ローカルで変数を扱うのに近い感覚です.
リアルタイムで反映され,オフライン対応もあるため,尚更です.

システム構成の変化

上記二項目と被る部分もありますが,従来はシステムによる制約がありました.

これまでユーザからデータベースまでは,少なくともウェブサーバ,あるいはウェブ・アプリケーションサーバを挟む必要がありました.
場合によってはもっと多くなっていたでしょう.
なのでソフトウェア的にも層状になるのは然るべきだったのです.

しかしFirebaseのデータベースは,直接書き込みが可能です.
これにより,ソフトウェア設計に新たな活路ができたわけです.
つまり,一気通貫するような設計が容易に可能となったわけなのです.

Googleは数で殴るのが得意

Googleがとあるサービス2を作る時の逸話に,こんなものがあります.
「すぐに結果を返して欲しいなら,5000台同時に動かせばいいかなぁ」

Firebaseの新しいデータベースであるFirestoreも,このような考えに基づいているDatastoreが基盤になっています.
10個のデータを取り出したい時,元データが100でも100万でもパフォーマンスが落ちないというとんだ化け物です.
Googleだからこそできる代物であり,この技術がFirebaseに組み込まれることによって(容易に扱えるようになって),従来では無茶だった設計もしやすくなりました.

単一ファイルコンポーネント.vueを拡張

Vue.jsには単一ファイルコンポーネントという概念があります.
と言っても全く難しいものではなく,html, js, cssを一ファイルにまとめて扱える,というだけの話です.

それだけなのですが,機能毎にまとめ直すことによって,ファイル間の行き来が不要になり,より一貫性が出て生産性が向上します.
この考え方をデータベースにまで適用できたら,更に素敵ですよね.

元々分散的に書くもの

というか,Firebaseのデータベースは分散的に書くのが普通でした.
これをちょっと推し進めて視点を変えたのが今回の話になります.

 
ちょっと長くなってしまいましたが,
・FirebaseならSPA/ネイティブアプリ開発が容易
・ローカルのような感覚で使えるデータベース
・一気通貫な設計ができるように
・Firestoreでは無茶しやすく
・Vueのコンポーネント毎の考え方を拡張したらどうだろう
・Firebaseの流儀を拡張したらどうだろう
これらの条件が揃ったために「ユーザ画面とデータベースの構造を一致させること」が現実味を帯びてきたのでは,と考えます.

手順

現在,Firebaseの設計についてまとめてやちまモデルを提唱しようと思っています.
その根幹になるのが,今回の本題でもある View1st です.
(ちなみに思想の根本としては割り切りが大切)

View1st in やちまモデル

データベースの設計はしないでください.
まずユーザ画面を作ります.

(やる夫AA)
頭おかしいんじゃないかと思うかもしれませんが,頭がおかしいのは承知です.
続けましょう.
先ほどの背景事情を思い出してください,大丈夫です.

そして,そのユーザ画面に必要な情報をそれぞれデータベースに置くようにします.
データベースから取って表示するだけ,くらいで済むように準備して保存してしまう,というのがポイントです.

ユーザの見えるままに設計
(より正確には「コンポーネント(機能のひとまとまり)に一致させるよう設計」)
それだけです.
シンプルですよね.
しかしこれは強力です.

コンポーネントとデータベースの構造が一致し,ソースコードもシンプルになるため,システムが大規模になっても開発保守は容易です.

書き込みの工夫

もちろん,それぞれのコンポーネント内でデータの流れが完結することは珍しいでしょう.
そのため,必要に応じて他コンポーネントのデータベースも書き換える必要があります3
 
しかし,各コンポーネントから直接書き換えていては,それこそ今後の開発保守が大変になります.
複数箇所への分散書き込みさせるためのミドルウェアを作って,分散書き込みはそのミドルウェア経由のみで行うようにします.

結局のところ,従来の複雑さをこのミドルウェアに押し込んでいる,それによってソースコードの大半をシンプルに保っている,というのがポイントかもしれません.
このミドルウェアもこのミドルウェアで,見ればどこに書き込むかなどは一目瞭然,どこから呼び出されるかも書き込み先から推測しやすい4ので,捉えようによっては押し込まれたくせにシンプルです.
 
Vue使いであれば,storeパターンを拡張するのがいいでしょう.
書き込む先をFirebase上にするだけです.

Firebaseには現在2種類のデータベースがありますが(違いは過去記事など参照),よりFirestoreのほうが恩恵を受けやすいでしょう.
Firestoreを使う場合は,

firestore.js
setDBMessageAction (newValue, destination) {
  if (this.debug) console.log('setDBMessageAction triggered with', newValue)
  const batch = db.batch()
  const oneRef = db.collection("one").doc("path").collection("to").doc(destination)
  batch.set(oneRef, {hoge: newValue})
  const anotherRef = db.collection("another").doc("path").collection("to").doc(destination)
  batch.set(oneRef, {hoge: newValue})
  batch.commit()
}

Realtime Databaseを使う場合は,

realtimedb.js
setDBMessageAction (newValue, destination) {
  if (this.debug) console.log('setDBMessageAction triggered with', newValue)
  let sets = {}
  sets['/one/path/to/' + destination] = newValue
  sets['/another/path/to/' + destination] = newValue
  db.ref().set(sets)
}

などと書けばいいでしょう.
(もちろん,必要に応じデータ加工や取捨選択などは行っておきます)
各コンポーネントからは,これを呼び出すだけです.

さて,分散書き込みでそれぞれデータの整合性は保たれるのでしょうか?
上記のような書き込み命令を書いてやれば,Firebase側で対応してくれるのです.
つまり,すべて書き込むか,全く書き込まないか5,が保証されるのです.
上記についての詳細記事:Firestore
上記についての詳細記事:Realtime Database
 
では,もう少し具体的な場面を考えてみましょう.

具体例

例えばチャットアプリ.
LI○Eのようなものを考えると,チャットする画面とチャット一覧の画面がありますね.

両者では,「相手の名前・アイコン」「最新のコメント・時間」が共通するデータかと思います.
なのでそれぞれのコンポーネントに対応するデータベースに,すべて複製して書き込むことになります6
 
あるいはRSSリーダ.
ユーザ毎にフィード画面が異なりますね.
これはデータベースもユーザ毎にします(割り切り).
RSSをクロールした結果をそれぞれ書き込むわけです.

メリット

ソースコードがシンプルに

これは散々述べた通りですね.

大幅なコスト削減

ソースコードがシンプルになるため,大きく開発コストが削れます.
従来の方法で人員が多く割かれることは,それ自体が開発コストを押し上げます(意思疎通の手間や,分割できない仕事など).
これらを回避できるのです.

すり合わせのしやすさ

見た目から設計していくため,顧客や上層部との要求要件すり合わせはしやすいでしょう.
尚更開発速度は上がりやすいと思います.

Vuexすらいらない

Vue.jsには,Vuexというコンポーネント(機能のひとまとまり)間の状態管理・共有をしてくれる拡張フレームワークがあります7
しかしView1st8に則れば,そもそもコンポーネント間の状態管理・共有が不要になるわけです.
(現実的には,現在のユーザ情報くらいは保持させたほうが良いと思います.しかしその程度ならやはりstoreパターンで十分であって,Vuexまでは不要でしょう)

反論等

viewを変更する時はどうするか

確かに,dbと一致させてしまうとその問題があります.
しかし実際,データに関する部分まで変えることは頻繁ではないのではないでしょうか.

とは言え,全くないことでもないでしょう.
その時にはdbも書き換えてしまえばいいのです(割り切り).

以下のようにバージョンで区切っておくといいでしょう.

root
├hoge-component
|└ver
| ├1
| └2
├fuga-component
...

(破壊的な変更でなければバージョン変更の必要はありません)

@1amageek さんが以前発表されていたのですが,チェーン何たら9と言って,最新バージョンになければ一つ古いバージョンに取りに行く,それを見つかるまで繰り返す,という手法があります.
 
ネイティブアプリならば最適解でしょう.
ユーザがすぐにアプリを更新してくれるとは限らないので.
ウェブアプリであればクライアント側の更新がすぐ反映できるので,バッチでデータベースを再構築した後に,よいしょで一気に移行するのが楽です.

viewがない場合は

機械的に読み込む場合は,機械視点で読みやすい形に設計しておけば,一貫した設計になるかと思います.

n:nのリレーションはどうすべきか

単純に,愚直に両方から枝を生やせばいいのです(ちなみにFirebaseは基本的に階層型db).
なかなかの割り切り感ですが,これはやちまモデルに限らず,Firebaseの基本になります.

joinはどうするのか

そもそもView1stに従っていれば,大抵はjoinが不要なはずです.

クライアント側でjoinするという方法もあります.
データベースがシンプルになって動的な生成にも対応できるようになりますが,ソースコードは複雑になるので,どうかなぁって気持ちです.

あまりに複雑・動的な処理をしたい場合はBigQueryに投げちゃうのも手です(割り切り).
お互いGoogle同士なので容易に連携できます(公式サンプル,Firestoreのサンプルはまだ).
名付けるならば,BigFiVeスタックでしょうか.
なんて語感がいいんでしょう!

ただし,クエリによっては10秒単位で時間がかかる2場合もあります.
時間がかかっちゃう場合は,おとなしくクライアント側でごりごりデータ加工しましょう.

フィルタは

Realtime Databaseでも多少工夫すれば,複雑でないフィルタは可能でした(参考動画).
Firestoreではある程度のクエリが利用できるため,より容易です.
あまりに複雑・動的な処理をしたい場合は,BigQueryに(略)

検索は

さすがに検索は苦手です.
Algoliaというサービスと連携するのがいいでしょう(割り切り).
公式サンプル(Realtime DatabaseFirestore)もあります.
ElasticSearchもありですが,ちょっと手間ですね.

価格設定高い

確かに,単純にdbの保存にかかる料金だけを比較すると,Realtime Databaseは桁違いに高めかもしれません.
しかしお金で考えるならば,開発コストも考えなければなりません.

更に,Firestoreの場合は桁違いに安くなったので,一般的なAWS RDSやGoogle Cloud SQLなどと直接比較できてしまうくらいの価格設定となっています.

一般に,Realtime Databaseですら,その高価さ以上に開発コストの削減は見込めると考えます.
参考スライド(中盤あたりから)
Firestoreならば尚更です.

もし試算し,そうではなさそうとなった場合はぜひまさかってください.

db容量圧迫

複数箇所への同じ書き込みを行う以上は,dbの容量を消費しやすいです.

しかしFirebaseでは気にすべきは料金だけです.
容量の拡張などは自動でよしなにしてくれます.
料金についてはすぐ上で触れましたね.

従来のNoSQLでキャッシュしてるのと同じでは

詳しい方はこう思われるかもしれません.
言ってしまえばその通りです.

ただ,それをメインの方針とすること,明確な設計指針としてviewに合わせる,という意味では異なるかと思います.
それから,背景事情でも書いた通り,文脈も異なりますね.
これが,やちまモデルとしてわざわざ提唱を考えている簡単な理由です.

競合サービスではいけないのか

FirebaseはmBaaSと呼ばれるサービスです.
良くも悪くも,Parse亡き後はFirebase一強状態かと思います.

探せばありますが,情報は少ないですし,シェアが小さければ本番投下という名の負荷試験も少ないでしょう.
何より,Googleで培われた異次元な技術を手軽に利用できるのはFirebaseだけです.

良いものなら既に普及しているはず

背景事情で記載したような話があって,最近現実味を帯びてきた話です.
これから普及していくことでしょう.

SPA全般

Firebaseで,AndroidやiOSなどネイティブアプリでなく,ウェブでSPAとして開発する場合は,そのデメリットも引き継ぎやすいです.

ただ,開発コストが実は高くないのは冒頭でも述べた通りですし,
SEOやSNSカード等対応も,Cloud Functions for Firebase でサーバサイドレンダリングという技術を使えば問題ありません(Googleに関しては,クローラがjs対応済みなので何もする必要がありません).
初期ローディングに時間がかかるなども,分割した上で必要な時に読み込む機能が,少なくともWebpackVue.js(あるいはVue Router)にはあります.
Vue.js, Firebase共に伸び盛りな分野なので,今後より良くなっていくことでしょう.

SNSのタイムラインはどうするか

鋭い指摘ですね.
例えば人気の出てきたSNSで,1万人のフォロワーに対し,本当に1人ずつ1万箇所に書き込むべきなのでしょうか?

公式からは一応無難な手法が提示されています.
そして,やちまモデルに追加予定の別の方法があるのです!

公式の手法ほどローディングに時間がかからず,複雑にもならず,容量も消費しません.
そんなおいしい話があるのでしょうか?
単にFirestoreの性能をフル活用しているのです.

このあたりはまた後日記事にしますのでお楽しみに.

ひとまず今回のまとめ

FiVeスタックはいいぞ(Vue.js + Firebase)
(あるいはBigQueryを加えてBigFiVeスタック
やちまモデル
 その0 割り切れ!
 その1 View1st;ユーザの見えるままに設計,複雑さを押し込めろ
(より正確には「コンポーネント(機能のひとまとまり)に一致させるよう設計」)

そんなわけで,Vue.jsを使うならぜひFirebaseとペアにしましょうというお話でした().
その他,質問や鋭利なまさかりはお気軽にどうぞ.

コミュニティ

vuejs-jp

Firebase Japan User Group

関連過去記事

今回Vue.jsのアドベントカレンダーなのにFirebase推し推しだし過去記事も全然Vue.js自体はなかった10ですすいません
 
結局Firebaseのdbでは何に気をつけるべきか?少しマニアックな内容も含め,わかりやすく説明する(realtime database)(Firestoreでも大半は参考になると思います)
Firebase RTDB + GCP datastore = Firestoreについて第一印象
Firebase Dev Summit 2017 で発表された新機能ざっくり,これまでとの違いも
Firestoreに負荷試験(Loadroid)してみた+補足
 
 

 
 


  1. 難しい言葉で言えば,凝集度を高める,あるいは結合を疎にする,ということがデータベースのレイヤまで実現できてしまう,ということになるかと思います. 

  2. もしかしたら文脈からわかるかたもいる,かもしれません(いや,無理でしょう).BigQueryのことです. 

  3. いわゆる非正規化というやつに該当します. 

  4. コンポーネントとデータベースを一致させているため,大抵書き込み先と呼び出し元が一対一対応となるわけです. 

  5. 原子性と呼ばれるやつですね. 

  6. 厳密には,Firebaseのデータベースは画像や音声など保存するには向かないので,アイコンはCloud Storage for Firebaseに保管し,データベース内に保管先を書き込みます 

  7. Reactで言うところのFluxやRedux, MobXです. 

  8. 蛇足:うっかりVue1stと打ちそうになるけれど,あながち間違っていないかもしれません. 

  9. 名前忘れちゃった,ごめんなさい.しかもまたメンション飛ばしてすいません.素敵な手法だと思ったので. 

  10. まぁVue.jsは日本語ドキュメントあるし,記事もそこそこ見かけるしなぁ,というのはひとつあるのかもしれません.今後良さげなネタあったら書きたい.