😎

Rails6の複数DBを試してみた

2020/01/21に公開

Rails6からActive Record で複数のデータベースが利用できるようになった。

この機能はモノリシックなシステムをマイクロサービス化していく過程でも有効で、アプリケーションをロジック・データを順番に分けていく時に活用できる。(参考: IBM Developerサイト / マイクロサービス、SOA、API:味方か敵か / 図5.モノリシック・アプリケーションからマイクロサービスへ

今回はベースとなるアプリケーション(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

1つだとつまらないので、プードル(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">]>

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

新しいアプリケーション(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) ================

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

$ 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ガイドを参考に進める。

まずは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

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

$ 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

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

$ 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)から参照することができた。

所感

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

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

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

Discussion