39
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails で複数 DB の migration をする方法の調査と考察

Posted at

そもそもやるべきじゃないとか、すでに多くの人が調べているので今更感ありなどあるのですが :sweat_smile:

今回対象とする 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

記事など

ここに並べさせていただいた記事だけでも様々やり方は提案されていて、今回私が採用させてもらった方法もいろいろつまみ食いさせてもらっただけですね。先人たちの調査に感謝します :pray:

今回の調査の目的

  1. リードレプリカが利用できるので、アプリケーションからもそれを利用したい
  2. 元々は 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 ファイルに usingusing_group での設定をしていきます。

以下に migration 関連の拡張の実装があるのですが、ActiveRecord::Migration, ActiveRecord::Migrator と migration のコアなクラスも積極的に拡張することで実現しています。

自分で実装するには辛そうですね :sweat_smile:

以降では拡張は極力抑えて複数 DB の migration をする方法をみていきます。

migration に関わる設定ポイント

いくつかの設定ポイントがあるので一つ一つ見ていきましょう。

:pencil: Rails.application.config.paths

つまり、これらだけでほとんどの設定変更が可能です。

:pencil: ActiveRecord::Migrator.migrations_paths

migrations_pathsattr_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'] から読まれることを期待できます。

つまり、 Rails.application.config.paths の方で設定変更を済ませており、rake db:migrate のような方法で migration をするならば、特にここでの設定変更は不要となります。

:pencil: ActiveRecord::Tasks::DatabaseTasks

上記を見て分かる通り多くの設定項目があります。

つまり、いずれの項目も Rake タスクの db:load_config が呼ばれることを前提にすれば Rails.application.config.paths の設定をすることで ActiveRecord::Tasks::DatabaseTasks の設定を直接行う必要はなくなります。

:pencil: 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_pathsActiveRecord::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

secondbase用のdatabase.yml
development:
  database: xxx
test:
  database: xxx
production:
  database: xxx

secondbase:
  development:
    database: yyy
  test:
    database:yyy

少しそれますが second_baseRake::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.ymldatabase.yml の両方に user DB への接続情報を書くことになってしまう :weary:

実行時に接続先を変えてしまう

switch_pointdatabase.ymldevelopment_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:createdb:drop (他には purge, schema:load) はタスクの実行時にその後の処理で接続を自身で行うため、Rake タスクで事前に行っていてもそれは無視されてしまいます。

つまり、database.yml の中での Rails.env に対応するものしか使われないことになります。

ここでは create の部分だけコードを追っておきます。

ちなみに PostgreSQL の方だとデフォルトだと内部での接続は行わないようなので、挙動は異なっています。
https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb#L14-L15

create や drop の対応も可能か?

:pencil: ActiveRecord::Base.configurations の直接設定

second_base がやっているように ActiveRecord::Base.configurations を直接変更してしまえば、ActiveRecord::Tasks::DatabaseTasks.create_current 内で読み出される DB 情報もこちらの意図したものにできます。

この場合は db:load_config タスクが依存として実行されると、再度変更されてしまうため、Rake::Task の実行は execute で実行する必要があります :bulb:

多少実装内容に依存した方法にはなりますが、これで対応可能です。

一つ注意として、execute で依存タスクを実行しないようにすると、load_configenvironment 以外に依存するタスクは動作に支障がでます。
migration 系でいえば db:migrate:resettask :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

:pencil: ENV['DATABASE_URL'] を使う

あえて Rails から提供されている方法を使うならば EVN['DATABASE_URL'] が使えます。

こちらであれば実装には依存しない(もちろん仕様変更はあると思いますが)ので、ある程度安心して利用できます。一方で development と test を同時に実行してくれるような恩恵も得られなくなります。
加えて create や drop といった一部だけではありますが、DATABASE_URL="mysql2://root:root@localhost/development_user" db:create のように typo しそうな環境変数を付けなくてはいけなくなってしまいます。(まあ、なんらかスクリプトにするでしょうけど)

:pencil: database.yml は DB 毎に分離はしておくが、できるだけ DRY にしておく

ActiveRecord::Base.configurations の初期化タイミングの実装依存になるのも、DATABASE_URL を付けるのもやりたくなければ、複数になった database.yml 自体を DRY にしてしまうという手もあります。

例えば intializer に以下を用意します。

config/initializers/multi_database_yaml.rb
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 は以下のように書けます。

config/database_user.yml
<%= MultiDatabaseYaml.slice('user') %>
config/database.yml
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 への接続を行うだけでは createdrop の対応ができずいくつか工夫するポイントがあるということでした。

次に、実際に複数 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 で実行したい

ここまで見てくるとなんとなく嫌な予感がしてくるのが分かるかもしれませんが、なかなかに面倒です :sweat: (シェルスクリプトで書いた方が楽そう)

例えば Rake::Task#reenable を使って、以下のように書いたとします。(すでにかなり辛いですが :sweat_smile:

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

39
33
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
39
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?