はじめに
現在携わっているとある Rails 5 プロジェクトでは、リクエストに応じて接続先の DB を切り替えるというマルチテナント構成を採用しています。具体的には URL のサブディレクトリに応じて切り替えています。DB の切り替えは Apartment という Gem を使っているのですが、Rails 6 からデフォルトで複数データベース接続機能が追加されるので試してみました。
例題
URL のサブディレクトリに応じて接続するデータベースを切り替えたい。
方法
実装
まず database.yml
を次のように書きます。common
が共通データベースで hidamari
, madomagi
がテナントのデータベースのイメージです。この状態で bin/rails db:migrate
を実行すると、各データベースで同じマイグレーションが実行されます。
default: &default
adapter: postgresql
encoding: unicode
pool: 5
username: postgres
password: postgres
development:
common:
<<: *default
database: rails6_app_common_development
hidamari:
<<: *default
database: rails6_app_hidamari_development
madomagi:
<<: *default
database: rails6_app_madomagi_development
次に config/application.rb
を編集します。
module Rails6App
class Application < Rails::Application
# 略
# lib/autoload を自動で読み込むようにする。
config.paths.add 'lib/autoload', eager_load: true
# ActiveRecord::Middleware::DatabaseSelector という Rack ミドルウェアを有効にする。
Rails.application.config.active_record.database_selector = {}
end
end
ただし、この ActiveRecord::Middleware::DatabaseSelector
というミドルウェアは「読み込み用と書き込み用のデータベースを用意して、HTTP メソッドに応じて接続を切り替える」という想定で設計されているため、今回の用途には合わないです。そこで独自のミドルウェアに置き換えます。
# サブディレクトリに応じて接続するデータベースを切り替えるための Rack ミドルウェア。
module Middleware
module DatabaseSelector
class SubDirectory
class << self
# URL のパスからサブディレクトリを抜き出す。
# ただし、そのサブディレクトリと同名のデータベース (spec_name) が存在しない場合は nil を返す。
def get_sub_directory(url)
uri = URI.parse(url)
sub_directory = uri.path[/[^\/]+/]&.to_sym
return nil unless sub_directory.in?(databases)
sub_directory
end
private
def databases
@databases ||=
ActiveRecord::Base
.configurations
.configs_for(env_name: Rails.env)
.map(&:spec_name)
.map(&:to_sym)
end
end
delegate :get_sub_directory, to: 'self.class'
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
select_database(request) do
@app.call(env)
end
end
private
def select_database(request)
database = get_sub_directory(request.url) || :common
ActiveRecord::Base.connected_to(database: database) do
yield
end
end
end
end
end
# ActiveRecord::Middleware::DatabaseSelector を独自の Rack ミドルウェアに置き換える。
Rails.application.config.middleware.swap(
ActiveRecord::Middleware::DatabaseSelector,
Middleware::DatabaseSelector::SubDirectory
)
# ルーティングのサブディレクトリを制限する。
class SubDirectoryConstraint
def initialize(include_common: false)
@include_common = include_common
end
def matches?(request)
sub_directory =
Middleware::DatabaseSelector::SubDirectory.get_sub_directory(request.url)
return false unless sub_directory
return false if !include_common? && sub_directory == :common
true
end
private
def include_common?
@include_common
end
end
Rails.application.routes.draw do
scope ':tenant', constraints: SubDirectoryConstraint.new do
resources :characters, only: :index
end
end
確認
テナントごとに異なるデータを用意します。
ActiveRecord::Base.connected_to(database: :hidamari) do
%w[ゆの 宮子 沙英 ヒロ]
.each do { |name| Character.create(name: name) }
end
ActiveRecord::Base.connected_to(database: :madomagi) do
%w[鹿目まどか 暁美ほむら 巴マミ 百江なぎさ 美樹さやか 佐倉杏子]
.each do { |name| Character.create(name: name) }
end
そして CharactersController#index
を実装して実際にアクセスしてみると、URL のサブディレクトリに応じてデータベースが切り替わっていることが確認できました。
TODO
- Active Job でデータベースを切り替える方法を検討する。
参考
-
rails/activerecord/lib/active_record/railtie.rb
- ActiveRecord::Middleware::DatabaseSelector を有効にする方法を調べた。
-
rails/activerecord/lib/active_record/middleware/database_selector.rb
- 独自の DatabaseSelector を実装する際に参考にした。