LoginSignup
26
13

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-05

はじめに

現在携わっているとある 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 でデータベースを切り替える方法を検討する。

参考

26
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
13