ブランチごとにDB切り替えるヤツ作った

Gitのブランチ名をもとにActiveRecordが接続するDBを切り替えるRubygemを作った。

naoty/brancher

使い方

group :development do
  gem "brancher"
end

Gemfileに書いてbundle installするだけ。あとは自動的にブランチごとに別々のDBが使われるようになる。

config/database.ymlでdevelopment環境のDB名をsample_app_devと指定していた場合、masterブランチならsample_app_dev_masterが使われるし、some_featureブランチならsample_app_dev_some_featureが使われる。

問題意識

複数のブランチを移りながら開発していると、migrationを実行したブランチとしてないブランチでDBのスキーマが不整合になってエラーをおこすことがよくある。そのたびにrake db:migrateだったりrake db:resetだったりrake db:schema:loadしたりするのが非常に面倒だった。そういった問題を解決するためにブランチごとにDBを分けられるツールを作った。

どう実現しているか

やっていることはconfig/database.ymlをロードしたオブジェクトをいじっているだけ。これをいじるタイミングは2つある。Railsアプリケーションの初期化時とdb:load_configタスクだ。

Railsアプリケーションを初期化する際、ActiveRecord::Baseがロードされたあとにestablish_connectionが実行される。このメソッドはconfig/database.ymlに基いてDBとのコネクションを接続するものなので、これが実行される前にDB名をブランチ名に従っていじってあげる必要がある。実際に実行されているコードは以下の通りだ。

# lib/active_record/railtie.rb

initializer "active_record.initialize_database" do |app|
  ActiveSupport.on_load(:active_record) do
    self.configurations = Rails.application.config.database_configuration

    begin
      establish_connection
      # ...
    end
  end
end

Rails.application.config.database_configurationはconfig/database.ymlの中身をERBで展開してYAMLとしてロードしたHashオブジェクトだ。これがself.configurationsにセットされてestablish_connectionが実行される(ここでのselfActiveRecord::Base)。よって、この初期化処理が実行される前にRails.application.config.database_configurationをいじればいい。

初期化処理の一連の流れに割り込むにはRails::Initializable.initializerメソッドのオプションを使う。そして、その中でRails.application.config.database_configurationの中身を上書きする。

# lib/brancher/railtie.rb

initializer "brancher.rename_database", before: "active_record.initialize_database" do
  Rails::Application::Configuration.send(:prepend, DatabaseConfigurationRenaming)
end

次に、db:load_configタスク内でもconfig/database.ymlをいじる必要がある。なぜかというと、rake db:createなどの一部のRakeタスクは上述の初期化処理が実行されないからだ。environmentタスクに依存しているタスクであれば、environmentタスク内で初期化処理が行われるため問題ない。一方、db:load_configタスクは(おそらく)すべてのDBに関連するRakeタスクが依存しているため、ここでDB名をいじってあげればいい。

# lib/brancher/railtie.rb

rake_tasks do
  namespace :db do
    task :load_config do
      DatabaseRenameService.rename!(ActiveRecord::Base.configurations)
    end
  end
end

Rakeタスクは通常のメソッドとは異なり、同名のタスクを定義しても上書きされることはない。先に定義された順に同名のタスクが実行される。

所感

上のような初期化処理の仕組みやRakeタスクの追加などは以前のエントリなどでRailsの内部を読み理解を深めることによって実現することができた。ブラックボックスの中身が見えてくると、こういったRails自体に関わる便利ツールを簡単に作ることができてしまう。引き続きRailsのソースコードリーディングは続けていきたい。