Rails6の複数DBを試してみた

Rails6から[Active Record で複数のデータベースが利用](https://railsguides.jp/active_record_multiple_databases.html)できるようになった。

この機能はモノリシックなシステムをマイクロサービス化していく過程でも有効で、アプリケーションをロジック・データを順番に分けていく時に活用できる。（参考： [IBM Developerサイト ／ マイクロサービス、SOA、API：味方か敵か ／ 図5.モノリシック・アプリケーションからマイクロサービスへ](https://www.ibm.com/developerworks/jp/websphere/library/techarticles/soa_microservices_architecture/index.html#9.0.2.Figure5.Frommonolithicapplicationstomicroservices|outline)）

今回はベースとなるアプリケーション（Dogs）を作成し、そのデータを別の新しいアプリケーション（Cats)から参照して処理するケースを仮定して試してみる。


## 環境

```
$ ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]
```

```
$ gem -v
3.0.6
```

```
$ rails -v
Rails 6.0.2.1
```
```
$ mysql -u root -e 'select version();'
+-----------+
| version() |
+-----------+
| 8.0.18    |
+-----------+
```


## ベースアプリケーション(Dogs)の作成

まずRailsアプリケーションを作成し、プロジェクトルートに移動する

```
$ rails new -T -d mysql dogs
$ cd dogs
```

次に何かデータを入れたいので、チワワ(Chihuahua)のモデルを作成する

```
$ bundle exec rails g model Chihuahua name:string

Running via Spring preloader in process 22237
      invoke  active_record
      create    db/migrate/20200119010243_create_chihuahuas.rb
      create    app/models/chihuahua.rb
```

１つだとつまらないので、プードル(Poodle)のモデルも作成する

```
$ bundle exec rails g model Poodle name:string

Running via Spring preloader in process 22245
      invoke  active_record
      create    db/migrate/20200119010313_create_poodles.rb
      create    app/models/poodle.rb
```

DBを作成・マイグレーションする

```
$ bundle exec rake db:create db:migrate

Created database 'dogs_development'
Created database 'dogs_test'
== 20200119010243 CreateChihuahuas: migrating =================================
-- create_table(:chihuahuas)
   -> 0.0098s
== 20200119010243 CreateChihuahuas: migrated (0.0099s) ========================

== 20200119010313 CreatePoodles: migrating ====================================
-- create_table(:poodles)
   -> 0.0116s
== 20200119010313 CreatePoodles: migrated (0.0117s) ===========================
```

チワワとプードルに各々データを登録する

```
$ bundle exec rails r 'Chihuahua.create!(name:"ちくわ")'
$ bundle exec rails r 'Chihuahua.create!(name:"わんこ")'
$ bundle exec rails r 'Poodle.create!(name:"しらたま")'
```

データが登録できたか確認する。

```
$ bundle exec rails r 'p Chihuahua.all'

Running via Spring preloader in process 22407
#<ActiveRecord::Relation [#<Chihuahua id: 1, name: "ちくわ", created_at: "2020-01-19 01:11:49", updated_at: "2020-01-19 01:11:49">, #<Chihuahua id: 2, name: "わんこ", created_at: "2020-01-19 01:11:50", updated_at: "2020-01-19 01:11:50">]>
```
```
$ bundle exec rails r 'p Poodle.all'

Running via Spring preloader in process 22409
#<ActiveRecord::Relation [#<Poodle id: 1, name: "しらたま", created_at: "2020-01-19 01:11:51", updated_at: "2020-01-19 01:11:51">]>
```

チワワに２件、プードルに１件登録されていることを確認する

## 新しいアプリケーション（Cats)の作成

別の新しいアプリケーション（Cats)を隣のパスに作成する

```
$ cd ../
$ rails new -T -d mysql cats
$ cd cats
```

そこにも同様にデータを入れたいので、アメリカンショートヘア(AmericanShortHair)のモデルを作成する

```
$ bundle exec rails g model AmericanShortHair name:string

Running via Spring preloader in process 22466
      invoke  active_record
      create    db/migrate/20200119011624_create_american_short_hairs.rb
      create    app/models/american_short_hair.rb
```

DBを作成・マイグレーションする

```
$ bundle exec rake db:create db:migrate

Running via Spring preloader in process 22466
      invoke  active_record
      create    db/migrate/20200119011624_create_american_short_hairs.rb
      create    app/models/american_short_hair.rb
Created database 'cats_development'
Created database 'cats_test'
== 20200119011624 CreateAmericanShortHairs: migrating =========================
-- create_table(:american_short_hairs)
   -> 0.0097s
== 20200119011624 CreateAmericanShortHairs: migrated (0.0098s) ================
```

アメリカンショートヘアに２件のデータを登録する

```
$ bundle exec rails r 'p AmericanShortHair.create!(name:"とら")'

Running via Spring preloader in process 22487
#<AmericanShortHair id: 1, name: "とら", created_at: "2020-01-19 01:16:55", updated_at: "2020-01-19 01:16:55">

$ bundle exec rails r 'p AmericanShortHair.create!(name:"たま")'

Running via Spring preloader in process 22489
#<AmericanShortHair id: 2, name: "たま", created_at: "2020-01-19 01:16:56", updated_at: "2020-01-19 01:16:56">
```

正常に登録できていることを確認する

```
$ bundle exec rails r 'p AmericanShortHair.all'

Running via Spring preloader in process 22492
#<ActiveRecord::Relation [#<AmericanShortHair id: 1, name: "とら", created_at: "2020-01-19 01:16:55", updated_at: "2020-01-19 01:16:55">, #<AmericanShortHair id: 2, name: "たま", created_at: "2020-01-19 01:16:56", updated_at: "2020-01-19 01:16:56">]>
```

## 新しいCatsアプリケーションからベースアプリケーション（Dogs)のデータを参照する

以降は[Railsガイド](https://railsguides.jp/active_record_multiple_databases.html)を参考に進める。

まずは`database.yml` をバックアップし、プレーンな設定のみ入れておく

```
$ mv config/database.yml{,.org}
$ cat <<'EOT' > config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

development:
  <<: *default
  database: cats_development
EOT
```

この状態でアメリカンショートヘアに登録されている２件のデータが表示されている

```
$ bundle exec rails r 'p AmericanShortHair.all'

Running via Spring preloader in process 22626
#<ActiveRecord::Relation [#<AmericanShortHair id: 1, name: "とら", created_at: "2020-01-19 01:16:55", updated_at: "2020-01-19 01:16:55">, #<AmericanShortHair id: 2, name: "たま", created_at: "2020-01-19 01:16:56", updated_at: "2020-01-19 01:16:56">]>
```

Catsのデータベースを`primary`として設定する

```
$ cat <<'EOT' > config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

development:
  primary:
    <<: *default
    database: cats_development
EOT
```

この状態でもちゃんとアメリカンショートヘアに登録されている２件のデータが表示されている

```
$ bundle exec rails r 'p AmericanShortHair.all'

Running via Spring preloader in process 22634
#<ActiveRecord::Relation [#<AmericanShortHair id: 1, name: "とら", created_at: "2020-01-19 01:16:55", updated_at: "2020-01-19 01:16:55">, #<AmericanShortHair id: 2, name: "たま", created_at: "2020-01-19 01:16:56", updated_at: "2020-01-19 01:16:56">]>
```

新しいCatsアプリケーションにDogsのモデルをコピーする

```
$ cp -rf ../dogs/app/models app/models/dogs
```

DogsのモデルはCatsとは別のデータベースを参照するので、基底クラスをApplicationRecordを継承した別クラスに置き換える

```
$ rm -rf app/models/dogs/application_record.rb app/models/dogs/concerns
$ cat <<'EOT' > app/models/dogs/base.rb
class Dogs::Base < ApplicationRecord
  self.abstract_class = true
  connects_to database: { writing: :dogs }
end
EOT
```

各モデルクラスを前項で作成したDogs::Baseを継承する形に変更し、Dogsモジュール配下に入れる

```
$ find app/models/dogs -type f -name '*.rb' -print0 | xargs -0 sed -i.bak -e "s/^\(class\) \([^ ]*\) <.*/\1 Dogs::\2 < Dogs::Base/g"
$ find app/models/dogs -type f -name '*.bak' -print0 | xargs -0 rm
```

新しいCatsアプリケーションにDogsのマイグレーションファイルをコピーする

```
$ cp -rf ../dogs/db/migrate db/dogs_migrate
```

最後に `database.yml` にdogsのデータベースを追加する

```
$ cat <<'EOT' > config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

development:
  primary:
    <<: *default
    database: cats_development
  dogs:
    <<: *default
    database: dogs_development
EOT
```

これで一通り設定は完了。
以降でデータが取得できることを確認していく。

もともとCatsアプリケーションにあったアメリカンショートヘアのデータが取得できている

```
$ bundle exec rails r 'p AmericanShortHair.all'

Running via Spring preloader in process 22723
#<ActiveRecord::Relation [#<AmericanShortHair id: 1, name: "とら", created_at: "2020-01-19 01:16:55", updated_at: "2020-01-19 01:16:55">, #<AmericanShortHair id: 2, name: "たま", created_at: "2020-01-19 01:16:56", updated_at: "2020-01-19 01:16:56">]>
```

加えて新しいCatsアプリケーションからDogsの各データが取得できるようになっている

```
$ bundle exec rails r 'p Dogs::Chihuahua.all'

Running via Spring preloader in process 23116
#<ActiveRecord::Relation [#<Dogs::Chihuahua id: 1, name: "ちくわ", created_at: "2020-01-19 01:11:49", updated_at: "2020-01-19 01:11:49">, #<Dogs::Chihuahua id: 2, name: "わんこ", created_at: "2020-01-19 01:11:50", updated_at: "2020-01-19 01:11:50">]>
```
```
$ bundle exec rails r 'p Dogs::Poodle.all'

Running via Spring preloader in process 23118
#<ActiveRecord::Relation [#<Dogs::Poodle id: 1, name: "しらたま", created_at: "2020-01-19 01:11:51", updated_at: "2020-01-19 01:11:51">]>
```

これでベースとなるアプリケーション（Dogs）のデータを、別の新しいアプリケーション（Cats)から参照することができた。

## 所感

これでデータストアを共通化した状態でロジックだけ新しいアプリに移すことが可能になるが、この実装ではリスクが高いと考える。

例えば、複数のアプリケーション１つのデータストアを触ることで、本来意図しないデータベースの更新が発生する（コールバック系の処理とか危なそう）であったり、参照しているモデルやマイグレーションファイルに更新が起きたとき、意図しない不整合が起きる（参照してる側が知らないうちにモデルやDBスキーマが変わってたとか・・・）であったりと言ったことが起きると考えられる。

この仕組みを導入するからには、最終的にはきっちりとしたマイクロサービスにしてデータの受け渡しはちゃんとAPI経由で行う仕切りにするか、モデルだけ別ライブラリに切り出してJavaのDAOクラス的な物として使うか……意外と後者側の方が使い勝手がいいかもしれない。