そもそもやるべきじゃないとか、すでに多くの人が調べているので今更感ありなどあるのですが
今回対象とする 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