Rails6.0から複数のデータベースを標準機能として利用できるようになりました
複数のデータベースを利用することで、例えばプロジェクトの規模が大きくなった時にスケールしやすくなることや、コネクション数を増やすことができるなどの利点があります
この記事ではRailsアプリケーションで2つのデータベースの利用と、データベースのreplicaの利用のやり方を試してみます
作成したソースコードをGitHubで公開しています
https://github.com/youichiro/rails-multiple-db-sandbox
やったこと
- 複数のデータベースの利用
- commonデータベースとschoolデータベースを作成
- それぞれのデータベースのモデルを作成
- primary / replicaを利用
- commonデータベースのreplicaと、schoolデータベースのreplicaを用意
- GETリクエストはreplicaが呼び出されることを確認
- 異なるデータベースのテーブル間のJOINはできないことを確認
複数データベース
複数データベースは1つのアプリケーションから複数のデータベースに接続してデータの読み書きを行う仕組みです
データベースAとデータベースBの2つのDBがあった時、Railsは呼び出すモデルに応じて接続するデータベースを切り替えることができます
primary / replica データベース
1つのデータベースに対して読み込み専用のreplica(複製)を用意しておき、リクエストに応じてprimaryとreplicaを切り替える仕組みです
データベースへのアクセスが多くなった時に、書き込み用DBと読み込み用DBに分けておくことでアクセスの負荷を分散することができます
RailsではPOST, PUT, PATCH, DELETEリクエストはprimaryに、直近の書き込みがなければGET, HEADリクエストはreplicaにアクセスするように自動的に切り替えられるようになっています
データベースの設定
次のようなデータベースを作成するときのconfig/database.yml
は以下のようになります
- commonデータベース
- commonデータベースのreplica
- schoolデータベース
- schoolデータベースのreplica
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データベースに接続するモデルのベースとなる抽象クラスを作成し、データベースの接続先の設定を記述します
class CommonBase < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :common, reading: :common_replica }
end
ApplicationRecordを継承したCommonBase
クラスを作成し、connects_to
で書き込み時のDBと読み込み時のDBを指定しています
schoolデータベースに接続するモデルの抽象クラスも同様に作成します
class SchoolBase < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :school, reading: :school_replica }
end
新しいモデルを作成する時にはCommonBase
かSchoolBase
のどちらかを継承するようにします
これによってモデルによってデータベースの接続先を切り替えることができます
モデルの作成
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の設定
-
Gemfile
にgem arproxy
を追加してbundle install
-
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
に以下を記述
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
show
$ curl localhost:3000/users/1
create
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "saito", "school_id": 1}' localhost:3000/users
update
$ curl -X PUT -H 'Content-Type: application/json' -d '{"name": "saito(updated)"}' localhost:3000/users/5
destroy
$ curl -X DELETE http://localhost:3000/users/5
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)
さいごに
Rails6.1からサポートされる予定のシャーディング機能が楽しみ