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

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

## 今回対象とする Rails での複数 DB

これらに当てはまらないなら、あまり参考にはならないかもしれません。

- __一つの Rails アプリ__ から複数の DB への接続がある
- それぞれの DB は重複のない異る table が定義されている （ __水平分散ではなく垂直分散__ ）
- レプリケーションについては考えない（または、自動で同期されるものとする）

## 調査した際の複数 DB 関連の参考資料

### Ruby Gems

調査中に出てきたものだけなので網羅したとかそういうものではないです。

|gem|README に書かれている紹介文|アクティブか？|
|:--|:--|:--|
|[thiagopradi/octopus](https://github.com/thiagopradi/octopus)|Database Sharding for ActiveRecord|o (ちょっと勢いが弱いか？）|
|[eagletmt/switch_point](https://github.com/eagletmt/switch_point)|Switching database connection between readonly one and writable one|o|
|[taskrabbit/makara](https://github.com/taskrabbit/makara)|A Read-Write Proxy for Connections|o|
|[instructure/switchman](https://github.com/instructure/switchman)|Switchman is an ActiveRecord extension for sharding your database.|o (アクティブだがREADMEに警告あり)|
|[hirocaster/activerecord-sharding](https://github.com/hirocaster/activerecord-sharding)|Database sharding library for ActiveRecord|o|
|[kenn/slavery](https://github.com/kenn/slavery)|Simple, conservative slave reads for ActiveRecord|o|
|[customink/secondbase](https://github.com/customink/secondbase)|Seamless second database integration for Rails.|o|
|[Smarre/multi_ar](https://github.com/Smarre/multi_ar)|Multi database migration support for ActiveRecord 4|o|
|[customink/encom_dbs](https://github.com/customink/encom_dbs)|Rails Multi-Database Best Practices Roundup|ブログ記事の実装(gemではない)|
|[azitabh/multi-database-migrations](https://github.com/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](https://github.com/runeleaf/acts_as_readonlyable)||x|
|[schoefmann/multi_db](https://github.com/schoefmann/multi_db)|Connection proxy for ActiveRecord for single master / multiple slave database deployments|x|
|[kovyrin/db-charmer](https://github.com/kovyrin/db-charmer)|ActiveRecord Connections Magic (slaves, multiple connections, sharding, etc) |x|
|[bkr/goa](https://github.com/bkr/goa)|Gem Oriented Architecture - Share ActiveRecord Models with a Rails Engine|x|

### 記事など

- [Rails Multi-Database Best Practices Roundup - CustomInk](http://technology.customink.com/blog/2015/06/22/rails-multi-database-best-practices-roundup/)
  - gem の方で紹介した secondbase の開発元。
  - gem の方で紹介したブログ記事の実装の記事の方でもある。
- [Establishing a Connection to a Non-Default Database in Rails 3.2 - I Like Stuff](http://ilikestuffblog.com/2012/09/21/establishing-a-connection-to-a-non-default-database-in-rails-3-2-2/)
- [Ruby on Rails Connect to Multiple Databases and Migrations - GeeKhmer](http://geekhmer.github.io/blog/2015/02/07/ruby-on-rails-connect-to-multiple-databases-and-migrations/)
- [One Rails App With Many Databases - Brandon Rice](http://www.blrice.net/blog/2016/04/09/one-rails-app-with-many-databases/)
- [Multiple-Databases In Single Rails Application - VINSOL](http://vinsol.com/blog/2015/04/28/multiple-databases-in-single-rails-application/)
- [Using Rails Migration on different database than standard “production” or “development” - stackoverflow](http://stackoverflow.com/questions/1404620/using-rails-migration-on-different-database-than-standard-production-or-devel/34292909#34292909)
- [複数databaseのmigration - Qiita](http://qiita.com/y1row/items/b05cc0d78dd6b671e16b)
- [複数のDBに対するマイグレーションのあれこれ - Qiita](http://qiita.com/hatappi/items/3c125c061e9926213f74)
- [Rails ActiveRecord Multiple Databases And Migrations - CHRIS OLIVER](http://excid3.com/blog/rails-activerecord-multiple-databases-and-migrations)
- [Ruby on Railsで複数DBやってみたい - Atrae Tech Blog](http://atraetech.tumblr.com/post/117501563408/ruby-on-rails%E3%81%A7%E8%A4%87%E6%95%B0db%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E3%81%84)
- [Rails Engineを使ってAPIと管理画面を分離する - Daichi HIRATA](http://blog.daich.org/2015/12/18/rails-engine/)
- [ブランチごとにDB切り替えるヤツ作った - AnyType](http://naoty.hatenablog.com/entry/2015/01/19/003242)

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

## 今回の調査の目的

1. リードレプリカが利用できるので、アプリケーションからもそれを利用したい
2. 元々は sharding の用途で octopus を利用していたが、最近になって sharding はやめた。一方で、今後も垂直分割された複数 DB は維持する必要があった
  - つまり、現時点で DB を一つにまとめるという選択肢までは取れない

octopus を使い続けたとしても [Replication + Sharding](https://github.com/thiagopradi/octopus#replication--sharding) の利用でおそらく 1 も満たすことはできたのですが、`using(:shard_name)` と書くのに疲れたというどうでもいい理由もあり、再考することになりました。

### switch_point の採用

リードレプリカとマスターへの振り分けは、[eagletmt/switch_point](https://github.com/eagletmt/switch_point) を採用することにしました。
ここの採用にそんなに多くの理由はなくて、日本の会社の採用事例があって、マスター・リードレプリカの接続先は１つずつという用途にも一致したので、チームにも説明しやすかったぐらいでしょうか。（加えてコードが小さくてありがたい）

### switch_point を利用した場合の DB migration 

switch_point の事例だと作者の方が所属しているクックパッド社の記事が見つかるのですが、そちらでは [winebarrel/ridgepole](https://github.com/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](http://www.blrice.net/blog/2016/04/09/one-rails-app-with-many-databases/)

### 最終的に一つの Rails アプリに 1 DB になるのを見据えて考える

この前提に立って調査をしていこうということになりました。

## 複数 DB での migration の方法

### (参考までに) octopus はどうしているか？

octopus は `db/migrate` は 1 つだけなのですが、その中の migration ファイルに `using` や `using_group` での設定をしていきます。

https://github.com/thiagopradi/octopus#migrations

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

https://github.com/thiagopradi/octopus/blob/master/lib/octopus/migration.rb

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

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

### migration に関わる設定ポイント

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

#### :memo: Rails.application.config.paths

- `db`, `db/migrate`, `db/seeds.rb` の各ディレクトリやファイルパスの設定
  - https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/railties/lib/rails/engine/configuration.rb#L58-L60
- `config/database` のパスの設定 (`database.yml` のパスです。）
  - https://github.com/rails/rails/blob/master/railties/lib/rails/application/configuration.rb#L114

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

#### :memo: 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 をするならば、特にここでの設定変更は不要となります。

#### :memo: ActiveRecord::Tasks::DatabaseTasks

http://api.rubyonrails.org/classes/ActiveRecord/Tasks/DatabaseTasks.html

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

- `db_dir`, `migration_paths` の設定
  - https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/tasks/database_tasks.rb#L70-L76
- `database_configuration` に関して
  - `db:load_config` タスクの実行時に `ActiveRecord::Base.configurations` に移される
      - https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/railties/databases.rake#L15
  - `active_record/railtie` の同名のタスクで `Rails.application.config.database_configuration` から移されている
      - https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/railtie.rb#L35
  - `Rails.application.config.database_configuration` は `paths['config/database']` に指定されているファイルから読まれる。（デフォルトは database.yml ですね）
      - https://github.com/rails/rails/blob/master/railties/lib/rails/application/configuration.rb#L129-L143

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

#### :memo: 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` です。

https://github.com/rails/rails/blob/b326e82dc012d81e9698cb1f402502af1788c1e9/activerecord/lib/active_record/tasks/database_tasks.rb#L98-L105

上記のコードを見てもらうと分かる通りで `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

```yaml:secondbase用のdatabase.yml
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` が呼び出されません。（これは、おそらく、２つの DB に対して接続先は違えど同じタスクを実行するため、何度でも実行可能な execute を使っているのだと思います）
依存としては呼び出されませんが、`load_config` は直接実行されています。

参考: [rakeタスク内で別のタスクを呼び出す - Qiita](http://qiita.com/paty-fakename/items/5df189681c92ce1e8004)

### 設定の変更だけでたどり着く方法

すでに多くのブログ記事等で言及されている通りということですが、以下のようになります。

例えば以下のような構造にするとします。

```
+ 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 名の環境変数付きで実行するとすると以下のように書けます。

```rb
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](http://qiita.com/y1row/items/b05cc0d78dd6b671e16b)

### ここまでのまとめ

ここまでで、octopus のように Rails のコードに拡張を施さず、設定を変えるだけで実現できる別 DB への migration 方法を見てきました。

以降では、上記の方法では冗長な部分を削ったり、この方法でも失敗するようなケースを見ていきます。

## database.yml を一つにする

上述した単純な例では、`database.yml` が DB 毎に作成されることになります。
一方で `switch_point` は一つの `database.yml` に複数の設定を記述します。よって、もし上述の例でいく場合には、似た設定内容を持ったファイルが複数できてしまうことになります。

（もし user DB があった場合には、`database_user.yml` と `database.yml` の両方に user DB への接続情報を書くことになってしまう :weary: ）

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

`switch_point` の `database.yml` が `development_user:` のように `#{Rails.env}_#{database_name}' のようなキー名になっているとします。そうであるならば以下のようにするだけで目的は実現できます。

```diff
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` が呼ばれます
  - https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/railties/databases.rake#L26-L28
- `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
- `ActiveRecord::Tasks::DatabaseTasks.create` では各 database adapter 毎に振り分けられます （今回は MySQL を見ていきます）
  - https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/tasks/database_tasks.rb#L107-L117
- `ActiveRecord::Tasks::MySQLDatabaseTasks.create` が呼ばれるのですが、その内部で establish_connection が呼ばれているのが分かります。
  - https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/tasks/mysql_database_tasks.rb#L12-L36

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

### create や drop の対応も可能か？

#### :memo: ActiveRecord::Base.configurations の直接設定

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

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

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

>一つ注意として、`execute` で依存タスクを実行しないようにすると、`load_config`と `environment`  以外に依存するタスクは動作に支障がでます。
>migration 系でいえば `db:migrate:reset` が `task :reset => ['db:drop', 'db:create', 'db:migrate']` のような定義になっており、何もできないタスクになってしまいます。他にもいくつかのタスクが該当するため、注意する必要があります。

雑ですが、それを反映するとこのように書けます。

```diff
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
```

#### :memo: ENV['DATABASE_URL'] を使う

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

- `MergeAndResolveDefaultUrlConfig` で存在した場合のみ読み出されます
  - https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/connection_handling.rb#L61-L82
- `ActiveRecord::Base.configurations` で `MergeAndResolveDefaultUrlConfig` から設定を取り出す
  - https://github.com/rails/rails/blob/fb98d2e57162876c0e1823a5357bc44a932d08b9/activerecord/lib/active_record/core.rb#L46

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

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

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

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

```rb: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 は以下のように書けます。

```erb:config/database_user.yml
<%= MultiDatabaseYaml.slice('user') %>
```

```yaml: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 への接続を行うだけでは `create` や `drop` の対応ができずいくつか工夫するポイントがあるということでした。

次に、実際に複数 DB の migration を実際のアプリケーションに適用していく中で遭遇する事について触れていきます。

## 実際のアプリケーションに適用する中で遭遇すること

私が遭遇した事程度なので、多くはありません。

### ActiveRecord の load を Rails アプリの初期化よりも先に行う Gem の存在

例えば [rubysherpas/paranoia](https://github.com/rubysherpas/paranoia) は以下の箇所でしょっぱなから `active_record` を読み込んできます。

https://github.com/rubysherpas/paranoia/blob/core/lib/paranoia.rb#L1

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 の中で必要になるということですね。

```rb
ActiveRecord::Base.configurations = # すでに一度読み込まれてしまっているのでなんらか再設定
ActiveRecord::Base.establish_connection # 接続も初期化時にされてしまっているので、再設定した内容で再接続
```

> production だと無いですが、 development 環境だと bullet や factory_girl 等もこれらの状況を引き起こす要因になることがあります。

### 複数の DB migration を一つの Rake Task で実行したい

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

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

```rb
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 が分離しているケース__ の例を記載しておきます。

```rb
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` を使って書けるようになります。（少し前の節でもでてきましたが）


```diff
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` は何度でも実行できるため）

```diff
  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 は不要になります。

だいぶ雑な感じになりますが、以下のような感じでしょうか。（動くかな。。）

```rb
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