Edited at

Rails 6 の複数データベース接続機能でマルチテナントを試してみる


はじめに

現在携わっているとある Rails 5 プロジェクトでは、リクエストに応じて接続先の DB を切り替えるというマルチテナント構成を採用しています。具体的には URL のサブディレクトリに応じて切り替えています。DB の切り替えは Apartment という Gem を使っているのですが、Rails 6 からデフォルトで複数データベース接続機能が追加されるので試してみました。


例題

URL のサブディレクトリに応じて接続するデータベースを切り替えたい。


方法


実装

まず database.yml を次のように書きます。common が共通データベースで hidamari, madomagi がテナントのデータベースのイメージです。この状態で bin/rails db:migrate を実行すると、各データベースで同じマイグレーションが実行されます。


config/database.yml

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 を編集します。


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 メソッドに応じて接続を切り替える」という想定で設計されているため、今回の用途には合わないです。そこで独自のミドルウェアに置き換えます。


lib/autoload/middleware/database_selector/sub_directory.rb

# サブディレクトリに応じて接続するデータベースを切り替えるための 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



config/initializers/database_selector.rb

# ActiveRecord::Middleware::DatabaseSelector を独自の Rack ミドルウェアに置き換える。

Rails.application.config.middleware.swap(
ActiveRecord::Middleware::DatabaseSelector,
Middleware::DatabaseSelector::SubDirectory
)


config/routes.rb

# ルーティングのサブディレクトリを制限する。

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



確認

テナントごとに異なるデータを用意します。


db/seeds.rb

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 のサブディレクトリに応じてデータベースが切り替わっていることが確認できました。

01.png

02.png


TODO


  • Active Job でデータベースを切り替える方法を検討する。


参考