【Rails7】複数DBに接続し、DB毎にschemaやmigrationを管理したい

## はじめに
はじめまして！現在プログラミングスクール RUNTEQに通っています。
Railsの個人開発で複数DBで管理したい構成にしたかったので、その方法について練習しながらまとめてみました。
## 環境
- Ruby 3.2.2
- Ruby on Rails 7.0.7
- SQLite 

## 対象読者
- Railsで複数データベースを管理したい
- DB単位でスキーマファイルを管理したい
- DB単位でmigrateやrollbackをしたい

## database.ymlについて
DB切り替えの前に、RailsのDB管理について復習しておきます。

``rails new``した段階で、``config/database.yml``が生成されます。
``database.yml``とは**RailsがDBに接続するための情報を記載したファイル**です。
``rails new``でDBを指定しない場合、デフォルトでは**SQLite**が使われる仕様となっています。
(DBを変更したい場合は、``rails new app_name -d postgresql``のように指定します。)

初期段階のファイルの中身はこのようになっています。

```ruby:config/database.yml
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3
```

### 各コードの説明

```ruby
default: &default
```
&は**アンカー**と呼ばれ``&default``で指定しておくと、他の部分で``*default``として呼び出すことができる
```ruby
<<: *default
```
<<を**インジェクト**と呼び、アンカー指定した変数をマージすることができる。yml記号の一つ
```ruby
#1と2は一緒

# パターン1
development:
  <<: *default
  database: db/development.sqlite3

# パターン2
development:
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  database: db/development.sqlite3
```

```ruby
 adapter: sqlite3
```
DBの種類

```ruby
 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
```
同時に接続を使い回すことができる上限数。今回であれば5


```ruby
timeout: 5000
```
接続のタイムアウト時間

## 手順
基本的な手順はRailsガイドに詳しく書かれています。
- Active Record で複数のデータベース利用(https://railsguides.jp/active_record_multiple_databases.html)

簡単に手順をまとめると下記のようになりそうです。

1. 複数DBに接続するための設定
2. モデルとDBの紐付け
3. 複数DBのコマンド実行例
4. DB単位でmigrationの実行

2つのデータベースに接続することを仮定し、DB名はそれぞれfish,meatとします。
### 1. 複数DBに接続するための設定

```ruby:config/database.yml
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  fish:
    <<: *default
    database: fish_development
    migrations_paths: db/migrate_fish
  meat:
    <<: *default
    database: meat_development
    migrations_paths: db/migrate_meat

test:
  fish:
    <<: *default
    database: fish_test
    migrations_paths: db/migrate_fish
  meat:
    <<: *default
    database: meat_test
    migrations_paths: db/migrate_meat

production:
  fish:
    <<: *default
    database: fish_production
    migrations_paths: db/migrate_fish
  meat:
    <<: *default
    database: meat_production
    migrations_paths: db/migrate_meat
```
:::note
DB毎にmigrationを管理したい場合は、**migrations_paths**を用いてパスを指定する必要があります。
一方で、migrationの管理が必要ない場合は**database_tasks: false**と指定します。
:::

``rails db:create``を実行すると、複数のDBが作成されたことがわかります。
[![Image from Gyazo](https://i.gyazo.com/50ab1ea7f1f1614fdd898189a56bf96a.png)](https://gyazo.com/50ab1ea7f1f1614fdd898189a56bf96a)
### 2.モデルとDBの紐付け
DBに対応するモデルを名前空間で区別するためのディレクトリ構成を考えます。
**app/models**配下にそれぞれディレクトリとクラスを生成する構成です。名前空間で区切ることによって、テーブル名の衝突を防ぎます。

```ruby:ディレクトリ構成
- app
  - models
    - fish
      - fish.rb 
    - meat
      - meat.rb 
```

### 3. 複数DBのコマンド実行例
```ruby:コマンド実行例
## foodsモデルを作成
rails g model Fish::Food name:string --database fish

## foodsモデルを作成
rails g model Meat::Food name:string --database meat
```
上記コマンドを実行すると、**migrations_paths: db/migrate_xxx**配下にそれぞれファイルが作成されます。
```ruby:db/migrate_fish/xxxxxxxxxxxx_create_fish_foods.rb
class CreateFishFoods < ActiveRecord::Migration[7.0]
  def change
    create_table :fish_foods do |t|
      t.string :name

      t.timestamps
    end
  end
end

```

```ruby:db/migrate_fish/xxxxxxxxxxxx_create_meat_foods.rb
class CreateMeatFoods < ActiveRecord::Migration[7.0]
  def change
    create_table :meat_foods do |t|
      t.string :name

      t.timestamps
    end
  end
end
```

:::note warn
この時テーブル名が**DB名_テーブル名**となっている為、必要であれば**テーブル名**に変更が必要です。また``rails db:migrate``実行時にmodels配下に専用のmodule(テーブル名の接頭辞にDB名を付与するmodule)も作成されるので削除しておく必要があります。
:::

モデル作成時に**models**配下に``ApplicationRecord``を継承したベースとなるモデルファイル付が作成されます。
```ruby:app/models/meat_record.rb
class MeatRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :meat, reading: :meat }
end
```

```ruby:app/models/fish_record.rb
class FishRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :fish, reading: :fish }
end
```
``{ writing: :fish, reading: :fish }``のハッシュで書き込みと読み込みの権限について指定しています。

:::note warn
**self.abstract_class = true**の挙動については別途記事作成予定です。
:::

また、Railsはこのベースを継承した形でモデルファイルを生成してくれます。

```ruby:app/models/meat/food.rb
class Meat::Food < MeatRecord
end
```

```ruby:app/models/fish/food.rb
class Fish::Food < FoodRecord
end
```

### 4.DB単位でmigrationの実行
DB名を末尾に指定することで簡単に指定できます。
```ruby:migrate
rails db:migrate:fish
```
```ruby:rollback
rails db:rollback:meat
```
statusコマンドでもDB単位で表示してくれます。

```ruby:status
❯ rails db:migrate:status

database: fish_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20230905080236  Create fish foods


database: meat_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20230905080245  Create meat foods
```
schemaもDB毎に自動で管理されます。

```ruby:db/meat_schema.rb
ActiveRecord::Schema[7.0].define(version: 2023_09_05_082528) do
  create_table "foods", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end
```

```ruby:db/fish_schema.rb
ActiveRecord::Schema[7.0].define(version: 2023_09_05_082528) do
  create_table "foods", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end
```

実際にデータを作成できていることも確認できます。

```ruby:console
Loading development environment (Rails 7.0.7.2)
irb(main):001> Fish::Food.create(name: "tuna")
  TRANSACTION (0.1ms)  begin transaction
  Fish::Food Create (1.7ms)  INSERT INTO "foods" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "tuna"], ["created_at", "2023-09-05 08:37:43.464324"], ["updated_at", "2023-09-05 08:37:43.464324"]]
  TRANSACTION (0.7ms)  commit transaction
=> 
#<Fish::Food:0x000000010fa1ce20
 id: 1,
 name: "tuna",
 created_at: Tue, 05 Sep 2023 08:37:43.464324000 UTC +00:00,
 updated_at: Tue, 05 Sep 2023 08:37:43.464324000 UTC +00:00>
irb(main):002> Meat::Food.create(name: "beef")
  TRANSACTION (0.0ms)  begin transaction
  Meat::Food Create (1.6ms)  INSERT INTO "foods" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "beef"], ["created_at", "2023-09-05 08:38:03.926502"], ["updated_at", "2023-09-05 08:38:03.926502"]]
  TRANSACTION (0.9ms)  commit transaction
=> 
#<Meat::Food:0x0000000110a1cf50
 id: 1,
 name: "beef",
 created_at: Tue, 05 Sep 2023 08:38:03.926502000 UTC +00:00,
 updated_at: Tue, 05 Sep 2023 08:38:03.926502000 UTC +00:00>

```

## まとめ
DB周りを改めて学習できたので良い機会になりました！
最後まで読んでいただきありがとうございました！