Railsで複数DBに接続し、DB単位でスキーマやmigrationを管理する

# はじめに

Railsで複数データベースを利用するにあたり、ハマりポイントがいくつかあったので忘備録として残しておきます。

# 対象読者
- Railsで複数データベースを管理したい
- 複数データベースを使用する場合のモデルの取り扱い方法を知りたい。クラス名がコンフリクトするのを回避したい
- データベース単位でスキーマファイルを管理したい
- データベース単位でmigrateやrollbackをしたい
- 既に別リポジトリで管理しているデータベースに接続できるようにしたい。(が、migration管理はしたくない)

Railsで複数データベースを扱う記事は[公式ドキュメント](https://railsguides.jp/active_record_multiple_databases.html)含め多数見つかりましたが、上記を全て解決する記事がなかったので今回まとめることにしました。


# 今回やること
1リポジトリで3つのデータベースに接続。
わかりやすくするため、データベース名はcat, dog, birdとします。

```
cat: 別リポジトリで管理。migrateファイルは使用しない。
dog: 当リポジトリで管理
bird: 当リポジトリで管理
```

# 設定手順とコマンド実行事例
それでは具体的に、
- 複数データベースに接続するための設定
- モデルとデータベースの紐付け
- モデルのディレクトリ構成例
- データベースを操作するコマンド実行例

について説明していきます。

## 複数データベースに接続するための設定

初めに、config/database.ymlにて、複数データベースに接続するための設定をします。
defaultであれば環境名のネストがあるだけですが、複数データベースを使う場合、さらにデータベース名でネストさせます。

```config/database.ml
default: &default
  adapter: postgresql
  encoding: utf8
  url: <%= ENV['DATABASE_URL'] %>
  pool: 5

development:
  cat:
    <<: *default
    database: cat_development
    database_tasks: false
  dog:
    <<: *default
    database: dog_development
    migrations_paths: db/migrate_dog
  bird:
    <<: *default
    database: bird_development
    migrations_paths: db/migrate_bird

test:
  cat:
    <<: *default
    database: cat_test
    database_tasks: false
  dog:
    <<: *default
    database: dog_test
    migrations_paths: db/migrate_dog
  bird:
    <<: *default
    database: bird_test
    migrations_paths: db/migrate_cat

staging:
  cat:
    <<: *default
    database: cat_staging
    database_tasks: false
  dog:
    <<: *default
    database: dog_staging
    migrations_paths: db/migrate_dog
  bird:
    <<: *default
    database: bird_staging
    migrations_paths: db/migrate_bird
production:
  cat:
    <<: *default
    database: cat_production
    database_tasks: false
  dog:
    <<: *default
    database: dog_production
    migrations_paths: db/migrate_dog
  bird:
    <<: *default
    database: bird_production
    migrations_paths: db/migrate_bird

```

- migration管理したくない場合、`database_tasks: false` をつけるのがポイントです。([参考](https://railsguides.jp/active_record_multiple_databases.html#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%83%BB%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86%E3%82%92%E5%A4%96%E3%81%97%E3%81%A6%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B))

- 各データベースのmigrationファイルを管理するディレクトリは```migration_paths```で設定します。

今回、dogとbirdをmigration管理するので、以下のようなディレトリ構成としました。
```
- db
  - migrate_dog
  - migrate_bird 
```


## モデルとデータベースの紐付け
続いて、モデルとデータベースを紐付けます。

今回は、catデータベースをデフォルトのデータベースとして使うため、ApplicationRecordクラスの接続先をcatに指定します。

```app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :cat, reading: :cat }
end
```

これで、モデルはdefaultでcatデータベースを参照するようになりました。

次に、dogとbirdについても設定していきますが、ここからひと工夫が必要です。

### データベースに対応するモデルを名前空間で区別するためのディレクトリ構成
少し寄り道をして、複数データベースへの接続をモデルでどのように表現できるか考えてみます。
データベース間でテーブル名のコンフリクトを避けるべく、以下のように扱えると理想です。
```
Dog::User.create(name: 'わんちゃん')
Bird::User.crate(name: '鳥ちゃん')
```

実現のため、moduleを用いて名前空間を作ります。

以下のように、models下にデータベース用のディレクトリを作成し、
```
- app
  - models
    - dog
    - bird
```

データベース名のディレクトリ直下にのApplicationRecordもどきのBaseクラスを作成します。

```
- app
  - models
    - dog
      - base.rb 
    - bird
      - base.rb 
```

```app/models/dog/base.rb
class Dog::Base < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :dog, reading: :dog }
end
```

```app/models/bird/base.rb
class Bird::Base < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :bird, reading: :bird }
end
```

あとは、各Userクラスで上記を継承すればOKです。

```app/models/dog/user.rb
class Dog::User < Dog::Base
end
```
```app/models/bird/user.rb
class Bird::User < Bird::Base
end
```
※ 便宜上、`user.rb`のコードを書いてますが、上記ファイルは後の`rails g model`コマンドにより自動で生成されます。

これで、データベースに対応するモデルを名前空間で区別できるようになりました。

## データベースを操作するコマンド実行例
それでは実際に各データベースにusersテーブル(モデル)を作成していきます。
`--database`オプションでデータベースの指定が可能です。

```shell
## catデータベースにユーザーを作成
rails g model User name:string --database cat

## dogデータベースにユーザーを作成
rails g model Dog::User name:string --database dog

## birdデータベースにユーザーを作成
rails g model Bird::User name:string --database bird
```
- 名前空間をつけることで、各データベース用に作成したmodels下のディレクトリにモデルが生成されます。
- その場合、migrationファイルのテーブル名が、```dog_users```のようにデータベース名が接頭辞についてしまいます。気に入らない場合はmigrationファイルを編集して、テーブル名を`users`にしてください。

```timestamp_create_dog_users.rb
class CreateDogUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :dog_users do |t| ## ← "users"に変更
      t.string :name

      t.timestamps
    end
  end
end
```
- catデータベースはデフォルトのモデルを使用するため、名前空間は必要ありません。(※ もちろん、明示的にmodels/catディレクトリを作成すれば、`Cat::User`のように扱えます。)

上記を実行すると、```models/DB名/user.rb``` がそれぞれ作成されます。

加えて、Railsが親切に各データベース用のApplicationRecordを作ってくれているはずです。

```app/models/dog_records.rb
class DogRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :dog }
end
```

```models/DB名/user.rb```を見てみると、上記クラスを継承しています。
```models/dog/user.rb
class Dog::User < DogRecord
end
```

Railsがよしなに生成してくれたXXXRecordクラスを継承しても大丈夫ですが、私は同じ階層にbaseクラスを定義するのが好みなので、Dog::Baseを継承させました。(完全に好みの問題です)

```models/dog/user.rb
class Dog::User < Dog::Base
end
```

## データベース単位でmigrationの実行
いよいよmigrationを実行します。(DBが存在している前提です。)

`rails db:migrate:[DB名]`とすることでデータベース単位でmigrationを実行できます。

```shell
rails db:migrate:cat
```

もちろん、rollbackもできます。

```shell
rails db:rollback:cat
```

※ ```rollback:[DB名]```はRails6.1.0からサポートされました。私は当初、`6.0.3`で動かしており、rollbackだけデータベース指定できない沼にハマりました。([参考](https://github.com/rails/rails/commit/e33075a0efce63658111ab35ce9a9103b8b0d9b4))

## データベース単位でのスキーマファイル管理
migrationを実行すると、自動で[DB名]_schema.rbというファイルがそれぞれ生成されます。非常に便利ですね。
```
- db
  - cat_schema.rb
  - dog_schema.rb
  - bird_schema.rb 
```

## 挙動確認
rails consoleより以下を実行したところ、無事に各データベースにuserレコードが作成されることを確認できました。
```console
[1] pry(main)> Dog::User.create!(name: 'test')
  TRANSACTION (0.4ms)  BEGIN
  Dog::User Create (6.7ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "test"], ["created_at", "2022-12-20 07:27:27.997621"], ["updated_at", "2022-12-20 07:27:27.997621"]]
  TRANSACTION (3.3ms)  COMMIT
=> #<Dog::User:0x00007fa950501710 id: 1, name: "test", created_at: Tue, 20 Dec 2022 07:27:27.997621000 UTC +00:00, updated_at: Tue, 20 Dec 2022 07:27:27.997621000 UTC +00:00>
```

## まとめ
以上により、
- データベース名を名前空間として区別しながらのモデル操作
- データベース単位でスキーマファイルを管理
- データベース単位でのmigrateやrollback

を実現した上で、複数データベースを扱えるようになりました。
この記事がRailsで複数データベースを管理したい方にとって役に立てれば幸いです。