📝

Rails6.0で複数のデータベースを利用する

2020/10/31に公開

Rails6.0から複数のデータベースを標準機能として利用できるようになりました
複数のデータベースを利用することで、例えばプロジェクトの規模が大きくなった時にスケールしやすくなることや、コネクション数を増やすことができるなどの利点があります

この記事ではRailsアプリケーションで2つのデータベースの利用と、データベースのreplicaの利用のやり方を試してみます

作成したソースコードをGitHubで公開しています
https://github.com/youichiro/rails-multiple-db-sandbox

参考

https://railsguides.jp/active_record_multiple_databases.html

複数データベース

複数データベースは1つのアプリケーションから複数のデータベースに接続してデータの読み書きを行う仕組みです
データベースAとデータベースBの2つのDBがあった時、Railsは呼び出すモデルに応じて接続するデータベースを切り替えることができます

スクリーンショット 2020-10-31 4.17.26.png

primary / replica データベース

1つのデータベースに対して読み込み専用のreplica(複製)を用意しておき、リクエストに応じてprimaryとreplicaを切り替える仕組みです
データベースへのアクセスが多くなった時に、書き込み用DBと読み込み用DBに分けておくことでアクセスの負荷を分散することができます

RailsではPOST, PUT, PATCH, DELETEリクエストはprimaryに、直近の書き込みがなければGET, HEADリクエストはreplicaにアクセスするように自動的に切り替えられるようになっています

スクリーンショット 2020-10-31 14.17.32.png

データベースの設定

次のようなデータベースを作成するときのconfig/database.ymlは以下のようになります

  • commonデータベース
    • commonデータベースのreplica
  • schoolデータベース
    • schoolデータベースのreplica
config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost
  port: 3306

development:
  common:
    <<: *default
    database: rails_app_common_development
    migrations_paths: db/common_migrate
  common_replica:
    <<: *default
    database: rails_app_common_development
    replica: true
  school:
    <<: *default
    database: rails_app_school_development
    migrations_paths: db/school_migrate
  school_replica:
    <<: *default
    database: rails_app_school_development
    replica: true

primaryのデータベースにはmigrations_pathsにマイグレーションファイルの保存場所を指定しています
replicaにはreplica: trueを指定します

この設定でデータベースを作成します

$ bin/rails db:create

モデルの抽象クラスを作成

呼び出すモデルによって接続するデータベースを切り替えるようにします
commonデータベースに接続するモデルのベースとなる抽象クラスを作成し、データベースの接続先の設定を記述します

app/models/common_base.rb
class CommonBase < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :common, reading: :common_replica }
end

ApplicationRecordを継承したCommonBaseクラスを作成し、connects_toで書き込み時のDBと読み込み時のDBを指定しています

schoolデータベースに接続するモデルの抽象クラスも同様に作成します

app/models/school_base.rb
class SchoolBase < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :school, reading: :school_replica }
end

新しいモデルを作成する時にはCommonBaseSchoolBaseのどちらかを継承するようにします
これによってモデルによってデータベースの接続先を切り替えることができます

モデルの作成

commonデータベースにUserモデルを作成する場合の手順です
まずgenerate modelコマンドでモデルファイルとマイグレーションファイルを作成します

$ bin/rails g model user name:string school:references --database common

Running via Spring preloader in process 54763
      invoke  active_record
      create    db/common_migrate/20201030135726_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

マイグレーションファイルがdb/common_migrateディレクトリに作成されました
--databaseに接続するデータベースを指定することで、database.ymlで設定したmigrations_pathsに作成されるようになります
schoolデータベースにモデルを作成したい場合は--database schoolを指定します

次にマイグレートを行います

# 全てのマイグレーションファイルを適用する場合
$ bin/rails db:migrate

# commonデータベースのマイグレーションファイルのみを適用する場合
$ bin/rails db:migrate:common

最後にモデルファイルを変更します
生成時はApplicationRecordを継承していますが、Userモデルはcommonデータベースを利用したいのでCommonBaseを継承するように変更します

- class User < ApplicationRecord
+ class User < CommonBase
  end

これでUserモデルに関してはcommonデータベースから読み書きを行うようになります

リクエストによってprimary/replicaが切り替わっているか確認

replicaを用意することでPOST, PUT, DELETE, PATCHのリクエストはprimaryに書き込み、GET, HEADリクエストはreplicaから読み込むようになります
これを確認するために、arproxyを使用してクエリのログにデータベースの接続状況を表示するようにします

arproxyの設定

  • Gemfilegem arproxyを追加してbundle install
  • config/initializers/arproxy.rbに以下を記述
config/initializers/arproxy.rb
if Rails.env.development? || Rails.env.test?
 require 'multiple_database_connection_logger'
 Arproxy.configure do |config|
   config.adapter = 'mysql2'
   config.use MultipleDatabaseConnectionLogger
 end
 Arproxy.enable!
end
  • lib/multiple_database_connection_logger.rbに以下を記述
lib/multiple_database_connection_logger.rb
class MultipleDatabaseConnectionLogger < Arproxy::Base
 def execute(sql, name = nil)
  role = ActiveRecord::Base.current_role
  name = "#{name} [#{role}]"
  super(sql, name)
 end
end

リクエスト時のデータベース接続状況を確認

curlからリクエストを送信してログを見ると、呼び出されたのはwritingかreadingなのかを確認することができます
あらかじめ作成したusers_controllerで試します

index

$ curl localhost:3000/users

スクリーンショット 2020-10-31 18.40.58.png

show

$ curl localhost:3000/users/1

スクリーンショット 2020-10-31 18.41.09.png

create

$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "saito", "school_id": 1}' localhost:3000/users

スクリーンショット 2020-10-31 18.47.08.png

update

$ curl -X PUT -H 'Content-Type: application/json' -d '{"name": "saito(updated)"}' localhost:3000/users/5

スクリーンショット 2020-10-31 18.48.18.png

destroy

$ curl -X DELETE http://localhost:3000/users/5

スクリーンショット 2020-10-31 18.48.41.png

index, showアクションの場合はreading、create, update, destroyアクションの場合はwritingとなっており、primary / replicaが切り替わっていることがわかります

JOINの挙動を確認

同じデータベースのテーブル間はJOINできる

studentsテーブルをgradeテーブルにJOINする場合は、同じデータベースなのでJOINできます

Grade.joins(:students).where(name: 'grade1')

発行されるSQL

SELECT `grades`.*
FROM `grades`
INNER JOIN `students` ON `students`.`grade_id` = `grades`.`id`
WHERE `grades`.`name` = 'grade1

異なるデータベースのテーブル間はJOINできない

studentsテーブルをusersテーブルにJOINしようとした場合は、異なるデータベースなのでJOINできません

User.joins(:students).where(name: 'ogawa')

発生するエラー

ActiveRecord::StatementInvalid (Mysql2::Error: Table 'rails_app_common_development.students' doesn't exist)

Discussion