Railsで複数データベースを操作したいならOctopusが便利な件🐙

2020年10月31日

なぜOctopus?

とあるマルチテナント構成のシステムの管理画面の開発にあたり、複数のデータベースに接続して、システム全体で使用するマスタデータの操作や顧客情報の閲覧するという機能要件がありました。

複数DBに接続して操作できる方法として
  • Octopus
  • switch_point
  • Rails6の複数データベース接続
の3つが選択肢として上がりましたが、マルチテナントが動的に増えていくことや、接続情報をテーブルで管理するなどの要件もあって、Octopusを採用しました。

Octopusをシンプルに使うのであれば非常に簡単です。

シンプルな使い方

Gemfile
gem 'ar-octopus'

$ bundle install

database.yml
  octopus:
    shards:
      history_shards:
        aug2009:
          adapter: mysql
          host: localhost
          database: octopus_shard2
        aug2010:
          adapter: mysql
          host: localhost
          database: octopus_shard3
        aug2011:
          adapter: mysql
          host: localhost
          database: octopus_shard4

      country_shards:
        canada:
          adapter: mysql
          host: localhost
          database: octopus_shard5
        brazil:
          adapter: mysql
          host: localhost
          database: octopus_shard6
        russia:
          adapter: mysql
          host: localhost
          database: octopus_shard7

User.using(:canada).create!(:name => 'oi')
などと指定するだけで使うことができます。

たえだ、今回は動的にDB接続先が増えていくので、テーブル情報からDB接続情報を取得する必要がありました。


ちょっと複雑な使い方


今回登場するテーブル(イメージ)
# Table name: db_config
#
#  id                    :bigint(8)        not null, primary key
#  db_name               :string
#  db_host               :string
#  db_user_name          :string
#  db_password           :string

# Table name: companies
#
#  id                    :bigint(8)        not null, primary key
#  name                  :string
#  mail_address          :string
#  tell                  :string

# Table name: users
#
#  id                    :bigint(8)        not null, primary key
#  company_id            :bigint(8)
#  name                  :string
#  mail_address          :string
#  password              :string

config/initializers/octopus.rb
module Octopus
  class SelectSchema
    attr_accessor :shards, :master, :companies
    def initialize
      @shards = {:master => {}, :company => {}}
      @master = DbConfig.find_by(db_name: "master")
      @companies = DbConfig.group(:db_name).where.not(db_name: "master")
    end

    def execute
      begin
        select_master
        select_companies
        Octopus.setup do |config|
          config.environments = [:production, :development, :test]
          config.shards = shards
        end
      rescue ActiveRecord::StatementInvalid => e
        puts e
      end
    end

    def select_master
      return nil unless master.present?
      shards[:master]['admin'] = {
          :host => master.try(:db_host).gsub(/:(\d+)\//, ""),
          :adapter => 'mysql2',
          :database => master.try(:db_name),
          :username => master.try(:db_user_name),
          :password => master.try(:db_password),
          :port => 3306
        }
    end

    def select_companies
      return nil unless companies.present?
      companies.each do |shard|
        shards[:company][shard.db_name] = {
          :host => shard.try(:db_host).gsub(/:(\d+)\//, ""),
          :adapter => 'mysql2',
          :database => shard.try(:db_name),
          :username => shard.try(:db_user_name),
          :password => shard.try(:db_password),
          :port => 3306
        }
      end
    end
  end
end

Octopus::SelectSchema.new.execute

この設定後、rails serverを再起動すると、DB接続情報のテーブルを参照して、octopusが使用できます。
なぜ、moduleにして、呼び出しているかというと、Ocotpusはアプリケーションを再起動しないと、DB情報が更新されないため、
システム利用中に動的にDB接続情報が増えた場合に対応できなかったので、ユーザーログイン時やdb_configが増えたタイミングで、このmoduleを再実行する形にしました。

あとは、このような指定方法で使うことはできますが、、、
db_name = DbConfig.find_by(company_id: 1).db_name
Octopus.using(:db_name) do
  User.create(:name => "User")
  Client.create(:name => "Client")
end

この方法だと、使用する都度、DB名を取得しないといけないので、concernに処理を共通化しました。

app/models/concerns/octopus_using.rb
module OctopusUsing
  extend ActiveSupport::Concern

  def run_using_company(company_id)
    db_configs = DbConfig.using(:master).find_by(company_id: company_id)
    unless db_configs.nil?
      Octopus.using(db_configs.db_name) do
        yield
      end
    end
  end
end

このように共通化して、使いたいときは、modelかcontrollerにincludeしてから
run_using_company(company_id)
  User.find(1)
end
これで、company_idを指定してあげるとそのcompanyのdb情報にアクセスできるようになります。

注意点

長時間接続していないと、DBコネクションが切断される問題があるらしいので、次の記事を参考にして、コネクションの貼り直しを行いました。
https://qiita.com/sachaos/items/fb83c773c3a78a9c7202
ただ、記事の方法だと、ApplicationControllerのbefore_actionですべてのアクション前にコネクションを張り直しています。
記事通りに設定していたところ、Linuxのスレッドが増える問題が発生したので、ログインに成功した場合のみコネクションを張りなすようにしています。

ただ、Rails6になって、複数データベース操作ができるようになったので、今後は、OctopusからRails6の標準機能を使用することが多くなっていくかもしれません。
コメント