はじめに
例えば Qiita::Team のような、企業やチーム向けに提供するマルチテナントな Web サービスの場合、 テナントを分ける方法としてぱっと思いつくのは
- すべてのテーブルに tenant_id 列を追加し、tenant_Id で常に絞り込む
- テナントごとにデータベースを分ける
の2つ。
このうち tenant_id 列を追加する方法だと、使うのに苦労する gem がある。 Devise とか Devise とか Devise とか。いやまぁ、default_scope でやれないことはないかもしれないけど。
テナントごとにデータベースを分ける方法なら、上手くデータベースさえ切り替えられれば、 Devise もそのまま使えるはず。
データベースを分ける方法でマルチテナントを実現するための gem に Apartment がある。
これを試してみる。
まずはサンプルプロジェクト作成
rails new multi_tenant_sample --skip-bundle
Devise と Apartment をインストール
ユーザーもテナントごとに登録したい。 Devise と Apartment を組み合わせて実現出来るか挑戦。
Gemfile に
gem "devise" gem "apartment"
を追加し、下記のコマンドを順に実行。
bundle install --path vendor/bundle bundle exec rails g devise:install bundle exec rails g devise User bundle exec rake db:migrate bundle exec rails g apartment:install
テナントを作成
テナント情報を保存するモデルを用意する。 今回は簡略化のため、テナント名を保存するだけにしておく。
bundle exec rails g model tenant name:string
テナント情報は、デフォルトのデータベースにまとめて保存したい。 Apartment が Tenant を除外するように、config/initializer/apartment.rb を編集する。 あと、テナントのデータベース名一覧を取得するための設定も記述しておく。
# config/initializers/apartment.rb require 'apartment/elevators/subdomain' Apartment.configure do |config| # Tenant を除外 config.excluded_models = %w{Tenant} # ... (中略) ... # rake db:migrate 時にすべてのテナントのデータベース名を取得するための設定 config.tenant_names = lambda{ Tenant.pluck :name } end # Apartment でデフォルトでは、サブドメインでデータベースを切り替える Rails.application.config.middleware.use 'Apartment::Elevators::Subdomain'
tenant_names を設定していないとマイグレーションを実行できないので、先にやっておくこと。 そして
bundle exec rake db:migrate
を実行。
無事マイグレーションが実行できたら、デフォルトのデータベースにテスト用のテナント情報を登録する。
bundle exec rails c irb(main):001:0> Tenant.create(name:"foobar") irb(main):002:0> Tenant.create(name:"hogefuga")
テナントを登録したあと
bundle exec rake apartment:create bundle exec rake db:migrate
を実行すると、db 下に foobar.sqlite3 と hogefuga.sqlite3 が作成され、それぞれマイグレーションが実行される。
トップページを作成
確認用にトップページを作成する。
bundle exec rails g controller home index
トップページには登録されているユーザーの一欄を表示したいので、コントローラーとビューを下記のように修正する。
# app/controllers/home_controller.rb class HomeController < ApplicationController def index @users = User.all if user_signed_in? end end
<h1>Home#index</h1> <% if user_signed_in? %> <%= link_to "logout", destroy_user_session_path, method: :delete %> <ul> <% @users.each do |user| %> <li><%= user.email %></li> <% end %> </ul> <% else %> <%= link_to "login", new_user_session_path %> <% end %>
ルーティングも修正。
# config/routes.rb MultiTenantSample::Application.routes.draw do devise_for :users root "home#index" end
ローカル環境で動作確認
Pow を使って動作確認する。Pow のインストール手順は次の記事の通り。 rbenv 使っていると、ちょっと苦労する。
Pow の準備が終わったら、‾/.pow の下に rails プロジェクトへのシンボリックリンクを作成。
cd ‾/.pow ln -s ‾/Projects/multi_tenant_sample
『登録したテナントの名前=サブドメイン=テナントが利用するデータベースの名前』になっているので、 ブラウザで http://foobar.multi_tenant_sample.dev/users/sign_in にアクセスしてみる。
サインアップ。
登録されているユーザーの一欄が表示される。
次に http://hogefuga.multi_tenant_sample.dev にアクセス。
先ほどサインアップしたユーザーでログインしてみると
ログインに失敗し、ログイン画面に戻る。ちゃんとデータベースが切り替わっているみたいだ。
こちらでもサインアップ。
ユーザー一欄には、サインアップしたユーザーだけが上がっている。
まとめ
Apartment を使うことで、テナントごとにデータベースを分け、 サブドメインで切り替えることができた。 Devise に Apartment を組み合わせることで、テナントごとにユーザーを保存するのもうまくいった。
今後、Rails でマルチテナントな Web サービスを作るときは、まず Apartment を使うことにしよう。