ども、@kimihom です。
SaaS において悩ましいのが、企業毎のデータをどうやって管理していくかという話がある。企業毎にデータは完全に独立しているので、URL やデータそのものも独立している方が望ましいとされる。では SaaS においてデータベースの設計をどのようにやっていくといいのかについて、1つの方法を掲示したいと思う。
データを共通で扱う
私がオススメしたい方法はデータを共通で扱うやり方だ。これにより、Rails から個別のテーブル名にアクセスするといった複雑なテーブル接続からの悩みから解放される。そしてコードとしては至ってシンプルに実現できる点が利点である。
それ以外のデータも全て、Contracts
に紐付けた 1 対多 の関係にすることが大前提だ。そうすれば、案外データを個別に取得することは Rails で簡単に実現できる。例えば、1つの Contract
に対して複数の Invoice
を取得するには以下のようなコードで実現できる。
class InvoicesController < ApplicationController before_action :authenticate_user! def index @contract = current_user.contract @invoices = @contract.invoices end end
大事なのは、@contract.invoices
といったようにして Contract
に紐づいた Invoices
しか取って来ないようにすること。この制約を入れるだけで同じテーブルだけど契約毎に限定されたレコードを取ってくることができる。とてもシンプルでメンテナンスしやすいコードである。データ取得する全てのコードにおいて、この制約を取り入れる必要がある。以下の例を見ていただきたい。
class InvoicesController < ApplicationController before_action :authenticate_user! def show # NG! 全く関係ないログインユーザーが、他の契約の Invoice を取って来れてしまう @invoice = Invoice.find(params[:id]) # OK. 他契約の invoice id を指定したとしても Not Found となる @contract = current_user.contract @invoice =@contract.invoices.find(params[:id]) end end
慣れてくれば Invoice
とそのまま呼び出すことに違和感を感じるようになる。そうすれば、間違っても NG なコードは書かなくなる。
他のデータ管理も共通の方法で管理する
例えば検索には Elasticsearch を使っている場合にも、同様の対応で実現が可能だ。Elasticsearch の場合、constat_score
を使って contract_id
を絞り込むことができる。
query = { :bool => { :must => [ { :constant_score => { :filter => { :term => { :contract_id => 1 } } } }, { :query_string => { :query => "aaa" } } ] } } Contact.__elasticsearch__.search(query)
そうすれば Elasticsearch 側にも複数インデックスを作るみたいな大変なことをする必要がなくなり、コードがたいへんシンプルになる。
考察
別テーブルや別データベース、別サーバーといった形で契約毎に分けるのと、上記のような共有テーブルの方法のメリットデメリットについて考えてみたい。
1つのテーブルで共有する場合に真っ先に思いつくデメリットは、1つの企業が例えば超大量データを保有した時に、他の企業にもパフォーマンスで影響が出てしまうという点だ。そのため、例えば基本的に契約したらデータは無制限に保存できるみたいなサービスだと、この心配事が常につきまとうだろう。例え大きめの契約企業がいたとしても、データ保存容量を平等にすることで、心の平穏を保たなければならない。 でも、この件に関してはあまり心配する必要がないと思う。なぜならそもそも何億データも保存して共通データをやりとりしている BtoC のサービスが既に存在するからだ。SaaS でも今後データが増えてくればテーブルの再設計やテーブル分割など必要になってくるかもしれないけど、そこまで悩むレベルになってきたらサービスは成功したということなのだ。データベースをより良いものにアップグレードするなり専門のデータベーススペシャリストを雇うなりできるだろう。
共有テーブルの他のデメリットとして意図しないデータの共有が発生してしまうリスクもある。1人で開発している分には先ほどの NG なコードは書かないだろうけど、誰かがコードを書いている時に 一般的なモデル#find をやらかすケースは当然起こり得るだろう。そもそもそんなコードを書かないような仕組みや意識付けが必要となってくる。
そして最後に、上記のテーブル設計は 1ユーザーに1つの契約しか所属することができない。それを許容するのなら良いに越したことはないけど、例えばログインした時に複数の契約にスイッチできるようなことを考え出すと、このテーブル設計は破綻する。Contract-User の設計を多対多にして URL などで契約を識別できるようにする必要があるだろう。
実際運営している私からの意見として、デメリットというか意識しなければならないのはその点くらいで、他で問題になったことは今まで一度もない。今後、何かしらで共有テーブルで問題が出てくるかもしれないけど、別テーブルや別データベースで設計した時よりは深刻な問題にはならないだろう。"早すぎる最適化は諸悪の根幹" を忘れずにやっていきたいところである。
終わりに
今回は SaaS における企業毎のデータ管理について 1つの方法を掲示した。この件に関して難しく考えるよりも、まずは顧客が欲しいと思ってくれる機能を開発することに集中して欲しいと思う。そしてデータが多くなってきて管理に悩むくらいになったら、次のステップとして最適化について悩んでいけば良いのだ。
てな訳で難しそうと思わずにまずは作ってみてみよう!