マルチテナントのアプリケーションを作っていて、サブドメインでアクセスしたらそのテナントのデータを表示したい。
apartment gem を使うのが一般的だと思うが、 builderscon tokyo 2018 で つらくないマルチテナンシーを求めて: 全て見せます! SmartHR データベース移行プロジェクトの裏側 という発表を聞いたので、せっかくなら organization_id を全テーブルに持たせる感じのデータ構造にしようと考えた。
以下のような作戦です。
(1) サブドメインを rack middleware で見て、 ActiveSupport::CurrentAttributes に保存する
class OrganizationInjector def initialize(app) @app = app end def call(env) req = ActionDispatch::Request.new(env) Current.organization = Organization.find_by!(subdomain: req.subdomain) @app.call(env) end end
(2) モデルでは default_scope
で制御する
class Article < AR::Base default_scope { Current.organization && where(organization: Current.organization) } end
というわけで
よっしゃ ActiveSupport::CurrentAttributes 使っていくぞ!!!
— Takafumi ONAKA (@onk) October 8, 2018
となりました。
検索して全部見る
使っていくぞ!!! と叫んでコードを書き出して、ふと
「Current.organization
作るんだったら current_user
じゃなくて Current.user
でも良いなぁ」
と思ったので、GitHub でひたすら ActiveSupport::CurrentAttributes
を使っているコードを検索して、検索結果を 100 ページまで全部見た。
このときに、ちょっとでも参考になる可能性があるな、と思ったらとりあえず ghq get
して 中を読む ようにしているんだけど、その過程で 3 つの良い出会いがあった。
(今回は検索結果 100 ページで中を見たのは 17 レポジトリなので、異常に S/N 比が悪い。検索力が低い……。
operator_recordable に出会った
https://github.com/yujideveloper/operator_recordable
record_with_operator gem、便利なのでメンテしてあげて欲しい……。 https://t.co/6eCDVImOL8 https://t.co/PShswFCPwI
— Takafumi ONAKA (@onk) September 13, 2017
CurrentAttributes は使わない?w https://t.co/j7FpvXJj16
— Takafumi ONAKA (@onk) September 14, 2017
と以前から妄想していた、record_with_operator の AS::CurrentAttributes 版があるのを知った。
動く実装がなくなって久しいので完全に最高。
rectify に出会った
https://github.com/andypike/rectify
導入しやすい trailblazer みたいなヤツです。
個人的には Rectify::Command の記法と、テストの stub 方法が充実しているのが気に入った。 まだ日本語の記事無いっぽいので狙い目。コードリーディング会で読むのにちょうど良いサイズだと思う。
動画も見た。 http://andypike.com/blog/conferences/rubyc-2016/
このレールは良いレールなので選んでも良いなぁ。
rails_multitenant に出会った
https://github.com/salsify/rails-multitenant
グローバルな変数に入れて default_scope でやるって発想は完全に同じで、
実装方法が AS::CurrentAttributes
ではなく自前なのが違う。
グローバルなコンテキストに GlobalContextRegistry
って名前を付けてるのが最高。触ると危ないヤツは危ないことが分かる名前であるべきだ。(なので AS::CurrentAttributes のクラス名は Current
ではない方がヨサソウ)
中身は Thread.current
ですね。
自分で Thread.current
扱ってるよりもみんなの目を通ったコードの方が安全だろう、というのが AS::CurrentAttributes を使おうと思ったキッカケなので、gemify されてるならコレにしようかな。サクッと読める (すぐ自前に乗り換えられる) サイズなのも良い。
salsify は salsify_rubocop がとても似た音楽性を持っていて好きな会社なので、gem を使えて嬉しい。
全部見た結果
current_user を Current.user にする必要があったら設計おかしいよねという結論になった。(知ってた
— Takafumi ONAKA (@onk) October 8, 2018
分かっていたけど監査ログとマルチテナント以外の使い道は無さそうだなぁ。(この用途で使いたいから別にいいんだけど。
— Takafumi ONAKA (@onk) October 8, 2018
という印象です。他の使い道がある場合は教えて欲しい。