そもそもやるべきじゃないとか、すでに多くの人が調べているので今更感ありなどあるのですが ![]()
今回対象とする Rails での複数 DB
これらに当てはまらないなら、あまり参考にはならないかもしれません。
- 一つの Rails アプリ から複数の DB への接続がある
 - それぞれの DB は重複のない異る table が定義されている ( 水平分散ではなく垂直分散 )
 - レプリケーションについては考えない(または、自動で同期されるものとする)
 
調査した際の複数 DB 関連の参考資料
Ruby Gems
調査中に出てきたものだけなので網羅したとかそういうものではないです。
| gem | README に書かれている紹介文 | アクティブか? | 
|---|---|---|
| thiagopradi/octopus | Database Sharding for ActiveRecord | o (ちょっと勢いが弱いか?) | 
| eagletmt/switch_point | Switching database connection between readonly one and writable one | o | 
| taskrabbit/makara | A Read-Write Proxy for Connections | o | 
| instructure/switchman | Switchman is an ActiveRecord extension for sharding your database. | o (アクティブだがREADMEに警告あり) | 
| hirocaster/activerecord-sharding | Database sharding library for ActiveRecord | o | 
| kenn/slavery | Simple, conservative slave reads for ActiveRecord | o | 
| customink/secondbase | Seamless second database integration for Rails. | o | 
| Smarre/multi_ar | Multi database migration support for ActiveRecord 4 | o | 
| customink/encom_dbs | Rails Multi-Database Best Practices Roundup | ブログ記事の実装(gemではない) | 
| azitabh/multi-database-migrations | A plugin to make it easier to host migrations for multiple databases in one rails app. This has been built for Rails 3.0.x. | x | 
| runeleaf/acts_as_readonlyable | x | |
| schoefmann/multi_db | Connection proxy for ActiveRecord for single master / multiple slave database deployments | x | 
| kovyrin/db-charmer | ActiveRecord Connections Magic (slaves, multiple connections, sharding, etc) | x | 
| bkr/goa | Gem Oriented Architecture - Share ActiveRecord Models with a Rails Engine | x | 
記事など
- 
Rails Multi-Database Best Practices Roundup - CustomInk
- gem の方で紹介した secondbase の開発元。
 - gem の方で紹介したブログ記事の実装の記事の方でもある。
 
 - Establishing a Connection to a Non-Default Database in Rails 3.2 - I Like Stuff
 - Ruby on Rails Connect to Multiple Databases and Migrations - GeeKhmer
 - One Rails App With Many Databases - Brandon Rice
 - Multiple-Databases In Single Rails Application - VINSOL
 - Using Rails Migration on different database than standard “production” or “development” - stackoverflow
 - 複数databaseのmigration - Qiita
 - 複数のDBに対するマイグレーションのあれこれ - Qiita
 - Rails ActiveRecord Multiple Databases And Migrations - CHRIS OLIVER
 - Ruby on Railsで複数DBやってみたい - Atrae Tech Blog
 - Rails Engineを使ってAPIと管理画面を分離する - Daichi HIRATA
 - ブランチごとにDB切り替えるヤツ作った - AnyType
 
ここに並べさせていただいた記事だけでも様々やり方は提案されていて、今回私が採用させてもらった方法もいろいろつまみ食いさせてもらっただけですね。先人たちの調査に感謝します ![]()
今回の調査の目的
- リードレプリカが利用できるので、アプリケーションからもそれを利用したい
 - 元々は sharding の用途で octopus を利用していたが、最近になって sharding はやめた。一方で、今後も垂直分割された複数 DB は維持する必要があった
 
- つまり、現時点で DB を一つにまとめるという選択肢までは取れない
 
octopus を使い続けたとしても Replication + Sharding の利用でおそらく 1 も満たすことはできたのですが、using(:shard_name) と書くのに疲れたというどうでもいい理由もあり、再考することになりました。
switch_point の採用
リードレプリカとマスターへの振り分けは、eagletmt/switch_point を採用することにしました。
ここの採用にそんなに多くの理由はなくて、日本の会社の採用事例があって、マスター・リードレプリカの接続先は1つずつという用途にも一致したので、チームにも説明しやすかったぐらいでしょうか。(加えてコードが小さくてありがたい)
switch_point を利用した場合の DB migration
switch_point の事例だと作者の方が所属しているクックパッド社の記事が見つかるのですが、そちらでは winebarrel/ridgepole という、Rails の migration とはまた異る仕組みを利用しているようです。
switch_point の他の例を探しても同様に ridgepole を使っているケースが見つかります。
これに乗るのも一つの手かなと考えました。
いずれは個別のサービスに分割したい
(とはいえサービスの成長次第ではそんなことはしないかもしれませんが、希望として)
現状、垂直分割された DB に属するテーブル群は、ある程度利用コンテキストやライフサイクルが別れた状態になっています。(例えばユーザー系の情報とかでまとまっている)
元々分割していた目的としても、DB インスタンスの分離等も考えていたこともあり、最終的に DB 毎の Rails アプリに分割して、Web API 連携させるという将来を考えるのもありだろうと。
また、直近では DB 毎にアプリケーションのコンテキストも分けられるなら、依存を減らすためにも Rails Engine に切り出していこうという流れもあり、そうなると一つ一つは 1アプリ 1 DBの Rails アプリとして考えてよくなります。
以下の記事を読んだのも一つのきっかけです。
One Rails App With Many Databases - Brandon Rice
最終的に一つの Rails アプリに 1 DB になるのを見据えて考える
この前提に立って調査をしていこうということになりました。
複数 DB での migration の方法
(参考までに) octopus はどうしているか?
octopus は db/migrate は 1 つだけなのですが、その中の migration ファイルに using や using_group での設定をしていきます。
以下に migration 関連の拡張の実装があるのですが、ActiveRecord::Migration, ActiveRecord::Migrator と migration のコアなクラスも積極的に拡張することで実現しています。
自分で実装するには辛そうですね ![]()
以降では拡張は極力抑えて複数 DB の migration をする方法をみていきます。
migration に関わる設定ポイント
いくつかの設定ポイントがあるので一つ一つ見ていきましょう。
 Rails.application.config.paths
- 
db,db/migrate,db/seeds.rbの各ディレクトリやファイルパスの設定 - 
config/databaseのパスの設定 (database.ymlのパスです。) 
つまり、これらだけでほとんどの設定変更が可能です。
 ActiveRecord::Migrator.migrations_paths
migrations_paths は attr_writer になっており設定可能なことがわかります
https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/migration.rb#L976
デフォルトは ['db/migrate'] ですね
https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/migration.rb#L1053
一方で、migration を Rake タスクの db:migrate で実行すれば db:load_config タスクが依存で呼ばれるのですが、それを前提にすれば、以下のように Rails.application.config.paths['db/migrate'] から読まれることを期待できます。
- https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/railties/databases.rake#L16
 - https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/tasks/database_tasks.rb#L74-L76
 
つまり、 Rails.application.config.paths の方で設定変更を済ませており、rake db:migrate のような方法で migration をするならば、特にここでの設定変更は不要となります。
 ActiveRecord::Tasks::DatabaseTasks
上記を見て分かる通り多くの設定項目があります。
- 
db_dir,migration_pathsの設定 - 
database_configurationに関して- 
db:load_configタスクの実行時にActiveRecord::Base.configurationsに移される - 
active_record/railtieの同名のタスクでRails.application.config.database_configurationから移されている - 
Rails.application.config.database_configurationはpaths['config/database']に指定されているファイルから読まれる。(デフォルトは database.yml ですね) 
 - 
 
つまり、いずれの項目も Rake タスクの db:load_config が呼ばれることを前提にすれば Rails.application.config.paths の設定をすることで ActiveRecord::Tasks::DatabaseTasks の設定を直接行う必要はなくなります。
 ENV["SCHEMA"]
schema.rb の出力先ですね。
デフォルトでは File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") が使われるようですが、この環境変数が設定されていればそちらが優先して使われるるようです。
https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/railties/databases.rake#L251
私は使っていないのですが、SQL が欲しい時に使う
db/structure.sqlの出力先を変える場合は ENV['DB_STRUCTURE'] に指定するようです。
(余談) second_base がどうしているか
second_base は以下のように SecondBase.on_base のブロック内で実行することで接続先を切り替えています。
https://github.com/customink/secondbase/blob/master/lib/second_base/databases.rake
SecondBase.on_base の実装を見てみると Rails.application.config.paths の設定は全く行わず、ActiveRecord::Migrator.migrations_paths と ActiveRecord::Tasks::DatabaseTasks の設定を行っています。
これまで出てきていない項目としては ActiveRecord::Tasks::DatabaseTasks.current_config です。
上記のコードを見てもらうと分かる通りで current_config は明示的な指定がなければ ActiveRecord::Base.configurations から読み出された値を使うようです。
また current_config にいれる値である config は、以下のコードを見ると分かる通り ActiveRecord::Base.configurations からキーを取得していますが、これは second_base が database.yml に secondbase: のキー配下に second_base 用の database 設定を記述するスタイルのため、そちらの内容を ActiveRecord::Base.configurations に適用しています。
https://github.com/customink/secondbase/blob/master/lib/second_base.rb#L15-L18
development:
  database: xxx
test:
  database: xxx
production:
  database: xxx
secondbase:
  development:
    database: yyy
  test:
    database:yyy
少しそれますが second_base は Rake::Task の実行方法が invoke ではなく execute であるため、依存タスクが実行されません 。つまり Rake::Task['db:migrate'] としても load_config が呼び出されません。(これは、おそらく、2つの DB に対して接続先は違えど同じタスクを実行するため、何度でも実行可能な execute を使っているのだと思います)
依存としては呼び出されませんが、load_config は直接実行されています。
参考: rakeタスク内で別のタスクを呼び出す - Qiita
設定の変更だけでたどり着く方法
すでに多くのブログ記事等で言及されている通りということですが、以下のようになります。
例えば以下のような構造にするとします。
+ app/
+ config/
  + database_main.yml
  + database_sub1.yml
  + database_sub2.yml
+ db/
  + migrate_main/
  + migrate_sub1/
  + migrate_sub2/
  + schema_main.rb
  + schema_sub1.rb
  + schema_sub2.rb
加えて DATABASE=sub1 bundle exec rake db:multi:migrate のように DB 名の環境変数付きで実行するとすると以下のように書けます。
namespace : db
  namespace :multi do
    task :set_custom_db_config_paths do
      database = ENV['DATABASE']
      ENV['SCHEMA'] = Rails.root.join("db/schema_#{database}.rb").to_s
      Rails.application.config.paths['db/migrate'] = [Rails.root.join("db/migrate_#{database}").to_s]
      Rails.application.config.paths['db/seeds.rb'] = [Rails.root.join("db/seeds_#{database}.rb").to_s]
      Rails.application.config.paths['config/database'] = [Rails.root.join("config/database_#{database}.yml").to_s]
    end
    multi_db_task = ->(name) {
      desc "Multi DB Migration db:#{name}"
      task name => [:environment, :set_custom_db_config_paths] do
        Rake::Task["db:#{name}"].invoke
      end
    }
    # NOTE 全てのタスクの確認はしていないので一部だけです
    %w(drop create purge schema:load migrate migrate:reset reset rollback).each do |task_name|
      multi_db_task[task_name]
    end
  end
end
例えば、各 DB 毎にタスクを用意するようにすれば DATABASE という環境変数は不要にできます。
また、上述のファイル構成も db ディレクトリ自体を DB 毎に分離することもできます。そうすれば schema.rb 等のファイルの名前を変える必要はありませんね。
そのあたりの部分はこちらがとても参考になります: 複数databaseのmigration - Qiita
ここまでのまとめ
ここまでで、octopus のように Rails のコードに拡張を施さず、設定を変えるだけで実現できる別 DB への migration 方法を見てきました。
以降では、上記の方法では冗長な部分を削ったり、この方法でも失敗するようなケースを見ていきます。
database.yml を一つにする
上述した単純な例では、database.yml が DB 毎に作成されることになります。
一方で switch_point は一つの database.yml に複数の設定を記述します。よって、もし上述の例でいく場合には、似た設定内容を持ったファイルが複数できてしまうことになります。
(もし user DB があった場合には、database_user.yml と database.yml の両方に user DB への接続情報を書くことになってしまう 
 )
実行時に接続先を変えてしまう
switch_point の database.yml が development_user: のように `#{Rails.env}_#{database_name}' のようなキー名になっているとします。そうであるならば以下のようにするだけで目的は実現できます。
namespace : db
  namespace :multi do
    task :set_custom_db_config_paths do
      database = ENV['DATABASE']
      ENV['SCHEMA'] = Rails.root.join("db/schema_#{database}.rb").to_s
      Rails.application.config.paths['db/migrate'] = [Rails.root.join("db/migrate_#{database}").to_s]
      Rails.application.config.paths['db/seeds.rb'] = [Rails.root.join("db/seeds_#{database}.rb").to_s]
-     Rails.application.config.paths['config/database'] = [Rails.root.join("config/database_#{database}.yml").to_s]
+     ActiveRecord::Base.establish_connection "#{Rails.env}_#{database}"
    end
    multi_db_task = ->(name) {
      desc "Multi DB Migration db:#{name}"
      task name => [:environment, :set_custom_db_config_paths] do
        Rake::Task["db:#{name}"].invoke
      end
    }
    # NOTE 全てのタスクの確認はしていないので一部だけです
    %w(drop create purge schema:load migrate migrate:reset reset rollback).each do |task_name|
      multi_db_task[task_name]
    end
  end
end
create や drop でうまくいかない
db:create や db:drop (他には purge, schema:load) はタスクの実行時にその後の処理で接続を自身で行うため、Rake タスクで事前に行っていてもそれは無視されてしまいます。
つまり、database.yml の中での Rails.env に対応するものしか使われないことになります。
ここでは create の部分だけコードを追っておきます。
- create 時には 
ActiveRecord::Tasks::DatabaseTasks.create_currentが呼ばれます - 
ActiveRecord::Tasks::DatabaseTasks.create_current内ではeach_current_configurationで Rails.env に応じて configuration が展開されます。developmentの時にtestも同時に実行されるがその用途です。- configuration は 
ActiveRecord::Base.configurationsから読み出されます - その環境毎に 
ActiveRecord::Tasks::DatabaseTasks.createに渡されます - https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/tasks/database_tasks.rb#L127-L132
 
 - configuration は 
 - 
ActiveRecord::Tasks::DatabaseTasks.createでは各 database adapter 毎に振り分けられます (今回は MySQL を見ていきます) - 
ActiveRecord::Tasks::MySQLDatabaseTasks.createが呼ばれるのですが、その内部で establish_connection が呼ばれているのが分かります。 
ちなみに PostgreSQL の方だとデフォルトだと内部での接続は行わないようなので、挙動は異なっています。
https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb#L14-L15
create や drop の対応も可能か?
 ActiveRecord::Base.configurations の直接設定
second_base がやっているように ActiveRecord::Base.configurations を直接変更してしまえば、ActiveRecord::Tasks::DatabaseTasks.create_current 内で読み出される DB 情報もこちらの意図したものにできます。
この場合は db:load_config タスクが依存として実行されると、再度変更されてしまうため、Rake::Task の実行は execute で実行する必要があります ![]()
多少実装内容に依存した方法にはなりますが、これで対応可能です。
一つ注意として、
executeで依存タスクを実行しないようにすると、load_configとenvironment以外に依存するタスクは動作に支障がでます。
migration 系でいえばdb:migrate:resetがtask :reset => ['db:drop', 'db:create', 'db:migrate']のような定義になっており、何もできないタスクになってしまいます。他にもいくつかのタスクが該当するため、注意する必要があります。
雑ですが、それを反映するとこのように書けます。
namespace : db
  namespace :multi do
    task :set_custom_db_config_paths do
      database = ENV['DATABASE']
      ENV['SCHEMA'] = Rails.root.join("db/schema_#{database}.rb").to_s
      Rails.application.config.paths['db/migrate'] = [Rails.root.join("db/migrate_#{database}").to_s]
      Rails.application.config.paths['db/seeds.rb'] = [Rails.root.join("db/seeds_#{database}.rb").to_s]
+     ActiveRecord::Base.configurations = {Rails.env => ActiveRecord::Base.configurations["#{Rails.env}_#{database}"]}
      ActiveRecord::Base.establish_connection "#{Rails.env}_#{database}"
    end
    multi_db_task = ->(name) {
      desc "Multi DB Migration db:#{name}"
      task name => [:environment, :set_custom_db_config_paths] do
-       Rake::Task["db:#{name}"].invoke
+       Rake::Task["db:#{name}"].execute
      end
    }
    # NOTE 全てのタスクの確認はしていないので一部だけです
-   %w(drop create purge schema:load migrate migrate:reset reset rollback).each do |task_name|
+   %w(drop create purge schema:load migrate reset rollback).each do |task_name|
      multi_db_task[task_name]
    end
  end
end
 ENV['DATABASE_URL'] を使う
あえて Rails から提供されている方法を使うならば EVN['DATABASE_URL'] が使えます。
- 
MergeAndResolveDefaultUrlConfigで存在した場合のみ読み出されます - 
ActiveRecord::Base.configurationsでMergeAndResolveDefaultUrlConfigから設定を取り出す 
こちらであれば実装には依存しない(もちろん仕様変更はあると思いますが)ので、ある程度安心して利用できます。一方で development と test を同時に実行してくれるような恩恵も得られなくなります。
加えて create や drop といった一部だけではありますが、DATABASE_URL="mysql2://root:root@localhost/development_user" db:create のように typo しそうな環境変数を付けなくてはいけなくなってしまいます。(まあ、なんらかスクリプトにするでしょうけど)
 database.yml は DB 毎に分離はしておくが、できるだけ DRY にしておく
ActiveRecord::Base.configurations の初期化タイミングの実装依存になるのも、DATABASE_URL を付けるのもやりたくなければ、複数になった database.yml 自体を DRY にしてしまうという手もあります。
例えば intializer に以下を用意します。
module MultiDatabaseYaml
  def self.slice(database_name, default_config_file = 'database.yml')
    multi_db_config = YAML.load(ERB.new(Rails.root.join('config', default_config_file).read).result)
    %w(production development test).each_with_object({}) {|env, configs|
      config_key = "#{env}_#{database_name}"
      configs[env] = multi_db_config[config_key] if multi_db_config.key?(config_key)
    }.to_yaml
  end
end
その上で各 DB 毎の database.yml は以下のように書けます。
<%= MultiDatabaseYaml.slice('user') %>
development_user:
  adapter: mysql2
  database: user_master
development_user_slave:
  adapter: mysql2
  database: user_slave
test_user:
  adapter: mysql2
  database: user_master_test
この節の主旨からはそれますが、記述が重複するという点は解消できるかもしれません。
ここまでのまとめ
ここでは、「Rails の設定変更だけで別 DB への migration を実現しようとすると、 database.yml が複数になってしまうため、それを (switch_point で必要になる) 一つの database.yml にできないか」というお題に対して考えてきました。
単純に database.yml を一つにして ActiveRecord::Base.establish_connection で別 DB への接続を行うだけでは create や drop の対応ができずいくつか工夫するポイントがあるということでした。
次に、実際に複数 DB の migration を実際のアプリケーションに適用していく中で遭遇する事について触れていきます。
実際のアプリケーションに適用する中で遭遇すること
私が遭遇した事程度なので、多くはありません。
ActiveRecord の load を Rails アプリの初期化よりも先に行う Gem の存在
例えば rubysherpas/paranoia は以下の箇所でしょっぱなから active_record を読み込んできます。
lazy_load_hooks を使っておいてくれるとこの手の問題は発生しないのですが、こういった Gem に依存していると、Rake Task で Rails.application.config.paths 等の設定を変更する前に初期化が実行されてしまい、例えば以下の箇所が実行されることで ActiveRecord::Base.configurations が設定済になってしまいます。
https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/railtie.rb#L120-L139
そうなると、「ActiveRecord::Base.establish_connection を利用しない、最も単純に設定変更だけで行う方法」だと手遅れになってしまいます。
Rake Task 内で ActiveRecord::Base.configurations の再設定と、ActiveRecord::Base.establish_connection を行う方法を取らなくてはいけません。
これまでも何度か出てきていますが、以下のようなコードが Rake Task の中で必要になるということですね。
ActiveRecord::Base.configurations = # すでに一度読み込まれてしまっているのでなんらか再設定
ActiveRecord::Base.establish_connection # 接続も初期化時にされてしまっているので、再設定した内容で再接続
production だと無いですが、 development 環境だと bullet や factory_girl 等もこれらの状況を引き起こす要因になることがあります。
複数の DB migration を一つの Rake Task で実行したい
ここまで見てくるとなんとなく嫌な予感がしてくるのが分かるかもしれませんが、なかなかに面倒です 
 (シェルスクリプトで書いた方が楽そう)
例えば Rake::Task#reenable を使って、以下のように書いたとします。(すでにかなり辛いですが 
 )
namespace :db do
  namespace :multi do
    task :migrate_all do
      databases = %w(main sub1 sub2)
      databases.each do |database|
        ENV['DATABASE'] = database
        Rake::Task['db:multi:migrate'].reenable
        Rake::Task['db:multi:set_custom_db_config_paths'].reenable
        Rake::Task['db:migrate'].reenable
        Rake::Task['db:load_config'].reenable
        Rake::Task['db:multi:migrate'].invoke
      end
    end
  end
end
この Rake Task はこれまで例として出してきた内容では失敗してしまいます。
reenable されたとしても、設定された各種の値は、Railsアプリが起動した状態なのでそのままであるため、序盤で紹介した全ての設定値を set_custom_db_config_paths の中で毎回設定しなければいけません。
以下に DB 毎に database.yml が分離しているケース の例を記載しておきます。
namespace :db do
  namespace :multi do
    task :set_custom_db_config_paths do
      database = ENV['DATABASE']
      ENV['SCHEMA'] = Rails.root.join("db/schema_#{database}.rb").to_s
      Rails.application.config.paths['config/database'] = [Rails.root.join("config/database_#{database}.yml").to_s]
      # NOTE Rails.application.config.database_configuration は呼び出しのたびにRails.application.config.paths['config/database'] に指定されたファイルパスから読みにいきます。
      ActiveRecord::Base.configurations = Rails.application.config.database_configuration
      # NOTE load_config タスクが reenable で毎回呼ばれ、 ActiveRecord::Tasks::DatabaseTasks.migrations_paths の値が 
      #      ActiveRecord::Migrator.migrations_paths に入るのを期待して、ここにだけ migration のパスを指定します
      ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [Rails.root.join("db/migrate_#{database}").to_s]
      ActiveRecord::Base.establish_connection
    end
    multi_db_task = ->(name) {
      desc "Multi DB Migration db:#{name}"
      task name => [:environment, :set_custom_db_config_paths] do
        # NOTE load_config の呼び出しを期待するため invoke
        Rake::Task["db:#{name}"].invoke
      end
    }
database.yml を一つにするケースにおいても、あまり変わらず、 ActiveRecord::Base.configurations に設定する値を Rails.application.config.database_configuration そのままではなく、"#{Rails.env}_#{ENV['DATABASE']}" で slice してくる形にします。
reenable の数を少し減らす
先ほどの Rake Task の set_custom_db_config_paths を全く load_config に依存しない形にすると、以下のように execute を使って書けるようになります。(少し前の節でもでてきましたが)
namespace :db do
  namespace :multi do
    task :set_custom_db_config_paths do
      database = ENV['DATABASE']
      ENV['SCHEMA'] = Rails.root.join("db/schema_#{database}.rb").to_s
      Rails.application.config.paths['config/database'] = [Rails.root.join("config/database_#{database}.yml").to_s]
      # NOTE Rails.application.config.database_configuration は呼び出しのたびにRails.application.config.paths['config/database'] に指定されたファイルパスから読みにいきます。
      ActiveRecord::Base.configurations = Rails.application.config.database_configuration
-     # NOTE load_config タスクが reenable で毎回呼ばれ、 ActiveRecord::Tasks::DatabaseTasks.migrations_paths の値が 
-     #      ActiveRecord::Migrator.migrations_paths に入るのを期待して、ここにだけ migration のパスを指定します
+     # NOTE load_config で設定される内容を直接記載する
      ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [Rails.root.join("db/migrate_#{database}").to_s]
+     ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
      ActiveRecord::Base.establish_connection
    end
    multi_db_task = ->(name) {
      desc "Multi DB Migration db:#{name}"
      task name => [:environment, :set_custom_db_config_paths] do
-       # NOTE load_config の呼び出しを期待するため invoke
        Rake::Task["db:#{name}"].execute
      end
    }
こうなると、reenable の指定は少なくなります。( execute は何度でも実行できるため)
  Rake::Task['db:multi:migrate'].reenable
  Rake::Task['db:multi:set_custom_db_config_paths'].reenable
- Rake::Task['db:migrate'].reenable
- Rake::Task['db:load_config'].reenable
  Rake::Task['db:multi:migrate'].invoke
reenable の数をゼロにする
これは Rake::Task['db:multi:migrate'] の依存である db:multi:set_custom_db_config_paths の呼び出しをなくせば、Rake::Task['db:multi:migrate'] を execute で実行できるようになるため、reenable は不要になります。
だいぶ雑な感じになりますが、以下のような感じでしょうか。(動くかな。。)
namespace :db do
  namespace :multi do
    def set_custom_db_config_paths
      database = ENV['DATABASE']
      ENV['SCHEMA'] = Rails.root.join("db/schema_#{database}.rb").to_s
      Rails.application.config.paths['db/seeds.rb'] = [Rails.root.join("db/seeds_#{database}.rb").to_s]
      ActiveRecord::Base.configurations = {Rails.env => Rails.application.config.database_configuration["#{Rails.env}_#{database}"]}
      ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [Rails.root.join("db/migrate_#{database}").to_s]
      ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
      ActiveRecord::Base.establish_connection
      yield
    end
    multi_db_task = ->(name) {
      desc "Multi DB Migration db:#{name}"
      task name do
        set_custom_db_config_paths do
          Rake::Task["db:#{name}"].execute
        end
      end
    }
    %w(drop create purge schema:load migrate reset rollback).each do |task_name|
      multi_db_task[task_name]
    end
    task :migrate_all do
      %w(main sub1 sub2).each do |database|
        ENV['DATABASE'] = database
        # NOTE environment が呼ばれなくなるので呼び出しておく
        Rake::Task['environment'].invoke
        Rake::Task['db:multi:migrate'].execute
      end
    end
  end
end
second_base は似たような形ですが、さらに別 DB の migration 後に設定を戻すということをやっていますね。
https://github.com/customink/secondbase/blob/master/lib/second_base/on_base.rb
上記の例では migrate_all のようなタスクを定義しましたが、これでは db:migrate しか複数 DB の一括 migration ができないので、他のものも必要になりそうです。
ただ、 rollback はさすがに一括でやるケースは無いと思いますし、意図しない rollback になる可能性があるのでやらない方が良さそうです。
全体を通して
Rails が設定可能な箇所として提供している部分を見ていった後に、実際に switch_point で利用するような database.yml を使ってどのように複数 DB migration 用の Rake Task を書いていくかを見ていきました。
個人の想いとしては、Rails のバージョンアップ時にもできるだけ問題が出ないように、設定項目とそれに応じたファイル・ディレクトリ構成を作るだけに済ませたかったのですが、結果として依存 Gem の問題であったり、database.yml の内容の重複であったり、タスクをひとまとめにするいう作業を経ると、ある程度 Rails の実装に依存した形になってきました。
プロジェクトにあった構成を取れればなと思います。
最後に、動作を保証するものではないサンプルコードですが、以下に今回のサンプルの一部を置いておきます。
https://github.com/dany1468/multidb_migraiton_sample