RailsでパスによってDBを切り替えれないか

# 概要
Rails6からマルチデータベースを利用できるようになりました
リードレプリカ構成で負荷をうまく分散させたり、取得するモデル単位でデータベースのコネクションを切り替えることができるようになりました

同一のモデルを取得するけれど、パスによって取得するDBを切り替えることを試したので書き残します


今回試した内容の結論から書くと「**concernで無理やり解決した**」となります

## どうやってDBを切り替えるのか
切り替えたいモデルが継承している抽象クラスに`connects_to database`を記述すれば切り替えができます

下記のような切り替えたいモデルとして`Card`があったとき、それが継承している`ApplicationRecord`に対して
writingのDBとreadingのDBを指定すると、必要に応じて切り替わります
```ruby:app/models/card.rb
class Card < ApplicationRecord
end
```

```ruby:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

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

もう少し手順を1つ1つ確認していくと下記のようなことが必要です
1. database.ymlの設定
2. 該当のモデル・テーブル
3. 該当のモデルが継承している抽象クラスでのDB接続設定(↑で書いてある内容)

### database.ymlの設定
database.ymlの設定としては下記の通りです(一部省略)
紛らわしいですが、default配下のdefaultの設定を利用するので今回は`kanban_development`のDBへデータの変更削除などを行いに行きます

```yml:config/database.yml
default:
  default:
    port: 3306
    database: kanban_development
  renewal:
    port: 3306
    database: board_development
    migrations_paths: db/board_migrate
```

### 該当のモデル・テーブル
これはそのままですが下記のモデルの作成とテーブルを作成する必要があります
```ruby:app/models/card.rb
class Card < ApplicationRecord
end
```

```ruby:db/schema.rb
ActiveRecord::Schema[7.1].define(version: 2023_11_29_171139) do
  create_table "cards", charset: "utf8mb4", force: :cascade do |t|
    t.string "title"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end
```

### 該当のモデルが継承している抽象クラスでのDB接続設定
↑でも書いていた通り、`connects_to database:...`の設定が必要です
```ruby:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

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

`writing: :default`を`writing: :renewal`と変更すると
接続先のdatabaseが`kanban_development`→`board_development`となります

```yml:config/database.yml
default:
  default:
    port: 3306
    database: kanban_development
  renewal:
    port: 3306
    database: board_development
    migrations_paths: db/board_migrate
```

## 実際にパスで切り替えて行くにはどうしたか
1. database.ymlの設定と各DBに対して同一のmigrationを作成する
2. 異なる抽象クラスを用意する
3. 動的にクラスを変更するためcontrollerに対してmoduleを定義

大きく分けてこの3ステップくらいです

サンプルコードはこちらに上げています

https://github.com/yothio/rails_multi_db_test

### database.ymlの設定と各DBに対して同一のmigrationを作成する
database.ymlについては↑でも載せているものを利用します
kanban_developmentのmigraitonは`db/migrate`配下に作成していき
board_developmentのmaigrationは**migrations_paths**で指定してる `db/board_migrate`配下へ作成します
```yml:config/database.yml
default:
  default:
    port: 3306
    database: kanban_development
  renewal:
    port: 3306
    database: board_development
    migrations_paths: db/board_migrate
```

### 異なる抽象クラスを用意する
接続先の切り替えはモデルが継承している抽象クラスで定義するので異なる抽象クラスとそれぞれを継承したモデルが必要です
```ruby
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :default, reading: :default }
end

class Card::Content < ApplicationRecord
  self.table_name = 'contents'
end
```
```ruby
class RenewalRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :renewal }
end

class Post::Content < RenewalRecord
  self.table_name = 'contents'
end
```

### 動的にクラスを変更するためcontrollerに対してmoduleを定義
moduleとしてモジュールを利用した側での名前を利用して動的にデータを取得するモジュールを作成しました
```ruby:app/controllers/concerns/contents_controllerble.rb
module ContentsControllerble
  extend ActiveSupport::Concern

  included do
    def index
      @contents = content_model.all
    end

    private

    def content_model
      namespace = self.class.name.deconstantize
      "#{namespace}::Content".constantize
    end
  end
end
```

実際にこのモジュールを利用する側はincludeしているだけです

```ruby:各種controller
class Post::ContentsController < ApplicationController
  include ContentsControllerble
end

class Card::ContentsController < ApplicationController
  include ContentsControllerble
end
```

### 処理の流れ
- `/post/contents`にアクセスがあった場合
  - Post::ContentsControllerのindexアクションが実行
  - 実態はモジュールにあり、Post::Contentモデルのデータを取得する
  - Post::Contentモデルは`RenewalRecord`を継承している
    - `RenewalRecord`では接続先を`renewal_development`としている
  - `renewal_development`のcontentsテーブルのデータを取得してくる
- `/card/contents`にアクセスがあった場合
  - Card::ContentsControllerのindexアクションが実行
  - 実態はモジュールにあり、Card::Contentモデルのデータを取得する
  - Card::Contentモデルは`ApplicationRecord`を継承している
    - `ApplicationRecord`では接続先を`kanban_development`としている
  - `kanban_development`のcontentsテーブルのデータを取得してくる

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/6a5dfb05-692a-d63d-ad63-8e796a5f1b75.gif)


## まとめ
サンプルコードを見てもらうとわかりますが、結構モジュールを読み込むだけのファイルができます


https://github.com/yothio/rails_multi_db_test



冒頭に書いた通りconcernで解決したのであまり良い結果とは言えません...
結局同一のテーブル構造を実現させるなら水平シャーディングを利用すると複数のDBに対して同一のデータが保存できそうです

ただ、その場合は複数DBでテーブル構造を共有するので各DBごとのカスタマイズ性が落ちそう
水平シャーディングについても聞きかじった程度なので見ていきます





## 参照・引用

https://railsguides.jp/active_record_multiple_databases.html