🌅

Rails 複数DBへの接続

2024/10/17に公開

はじめに

この記事では、Rails6.0から正式に導入された「マルチデータベース接続」について詳しく説明していきます。このマルチデータベース接続というのは、現在使用しているRailsアプリAから別サーバなどで動いているRailsアプリBへ接続を行い、データの取得やデータの変更を行うというものです。
下記の図を見ていただければ、イメージが湧きやすいかなと思います。

また、Rails6.0から正式に導入されたもののそれ以前のバージョンでも別アプリのDBへの接続などは可能でした。Rails6.0から正式に導入されたことによって、開発者側で行う設定が減って、Railsアプリがよしなにしてくれることが増えたという認識を持っていただければと思います。

他のDBへの接続手順

この章では、他のDBへの接続に必要な設定手順を説明していきます。
他のDBへの接続自体は難しいことはなく、行う設定自体は下記の三つになっています。

①database.ymlへの接続したいDBの設定追加

②追加した他DBへの接続を行うモデルの追加(抽象モデル)

③上記モデルを継承したテーブル単位のモデルの追加

一つ一つ見ていきます。

①database.ymlへの接続したいDBの設定追加

以降の例はこちらのRailsガイドを参考にしています。下記の例では、primaryというデフォルトのDBの設定に「animal」という他DBへの設定を追加した形になっています。「primary」という設定キーはRails側で用意されたものでこのDBがデフォルトのDBだと認識されます。

database.yml
production:
  primary:
    database: my_primary_database
    username: root
    password: <%= ENV['ROOT_PASSWORD'] %>
    adapter: mysql2
  animals:
    database: my_animals_database
    host: <%= ENV['ANIMALS_HOST'] %> #この設定は接続したいDBが別サーバで稼働している場合
    username: animals_root
    password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>
    adapter: mysql2
    migrations_paths: db/animals_migrate

database.ymlに上記のような設定を追加することで別アプリのDBへの接続などが可能になります。
(コメントアウトでも記載しましたが、もし接続したいDBが同じサーバで稼働しているならhostの設定は省くことができます。)

②追加した他DBへの接続を行うモデルの追加

次に行うのは、①で追加した他DBへのconectionを設立するモデルの作成です。
ActiveRecord::Baseを継承することで接続先DBのデータをRailsアプリで使える形に変換することができます。①の例の続きでモデルを作成すると下記のようになります。

animal_record.rb
class AnimalsRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :animals, reading: :animals_replica }
end

ここまでで、他データベースとの接続ができているかRailsコンソールを使って確認していきましょう!

AnimalRecord.connection.execute('SELECT DATABASE()')

このコマンドで先ほどdatabase.ymlファイルに設定した新たなDBへの設定が出力されるはずです。
ここで問題なくDBの設定が出力されれば接続は成功しています。

③上記モデルを継承したテーブル単位のモデルの追加

続いて、②で作成したモデルを継承して、必要なテーブル単位での各モデルを作成します。
イメージの話をすると①で他DBの設定を記述して、②で他DBとの接続を行う。そして、この③で必要なテーブルをモデル化してアプリ内でデータを活用できる形にするという流れです。
では、見ていきましょう!

LionRecord.rb
class LionRecord < AnimalsRecord
  self.table_name = "lions"
end

このモデルを設定することでアプリ内でLionRecordモデルを使うことができます。
このモデルは、AnimalRecordデータベース内にあるLionRecordテーブルのデータと使用することになります。

ここまでで、とりあえず他DBへの接続とそのDBのテーブルの利用まで行えるようになりました!
以降では、さらに細かい設定や接続するDBの利用方法によって異なる設定などについて記述していきます。

他DBへの接続のみで変更などを行わない時(readonly)

ここでは、接続先のDBのデータに変更を加えない時の設定について詳しく説明していきます。
ここでの変更を加えないというのは、HTTPメソッドでPOSTやPUT、DELETEといったレコードの変更を意味するメソッドからのリクエストがあるかどうかを意味します。
つまり、変更を行わないというのはGETメソッドなどのリクエストでデータの取得・読み取りしか行わないことを指します。このような場合は、以下のような設定を追加します。

LionRecord.rb
class LionRecord < AnimalsRecord
 self.readonly! #これで読み取り専用となる
  self.table_name = "lions"
end

この設定を追加することでLionRecordモデルに書き込み操作をするとActiveRecord::ReadOnlyRecord: Lion is marked as readonlyという例外が発生するようになります。この設定はテーブルを表すモデル単位でも可能ですが、もちろん他DBを表す抽象クラスでも全く同じ設定で読み取り専用にすることができます。

animal_record.rb
class AnimalsRecord < ApplicationRecord
  self.abstract_class = true
 self.readonly! #これで読み取り専用となる

  connects_to database: { reading: :animals_replica }
end

これによって、この抽象クラスを継承したクラスは全て読み取り専用となり、書き込みを行おうとすると例外を発生させます。

replicaを使った負荷分散

この章では、replicaデータベースを使用した負荷分散について説明していきます。
このreplicaデータベースというのは、本番DBをコピーしたデータベースになります。
主な使われ方としては、書き込みが行われる操作(witer)に関しては本番DB(primary)を使用して、読み取りが行われる操作(read)に関してはreplicaを使用するというものです。
これによって、一つのDBに全てのアクセスを集中させるのではなくアクセスを分散させることができて、アプリ全体でスムーズな動きを実現できたりします。
replicaの設定を追加するにはdatabase.ymlに下記のような記述を追加します。

database.yml
production:
  primary:
    database: my_primary_database
    username: root
    password: <%= ENV['ROOT_PASSWORD'] %>
    adapter: mysql2
  primary_replica:
    database: my_primary_database
    username: root_readonly
    password: <%= ENV['ROOT_READONLY_PASSWORD'] %>
    adapter: mysql2
    replica: true
  animals:
    database: my_animals_database
    username: animals_root
    password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>
    adapter: mysql2
    migrations_paths: []
  animals_replica:
    database: my_animals_database
    username: animals_readonly
    password: <%= ENV['ANIMALS_READONLY_PASSWORD'] %>
    adapter: mysql2
    replica: true

このようにdatabase.ymlにreplicaの設定を加えただけでは、まだreplicaデータベースが使われることはありません。次の章では、このreplicaデータベースをいつ使うのか、どのようにprimaryとreplicaを切り替えるのかについて説明していきます。

witerDBとreplicaDBの自動切り替え

ここでは、witerDBとreplicaDBの自動切り替えについて説明していきます。
この自動切り替えの設定を追加するとRailsアプリ側が、POST,PUT,DELETE,PATCHのいずれかのリクエストを受け取ると、自動的にwiterDBに書き込むようになります。また、リクエストがそれ以外のメソッドでも直近の書き込みがあった場合にはwiterDBが利用されます。
そして、これら以外のリクエストに関してはreplicaDBを使用するという切り替えが行われます。
だいぶ、Railsがよしなにしてくれることがわかります。

それでは、自動切り替えの設定追加を見ていきましょう。
まず、以下のコマンドを実行して、config/initializers/multi_db.rbという設定ファイルを作成しましょう。

bin/rails g active_record:multi_db

設定ファイルが作成できたら、以下の行のコメントアウトを外して自動切り替えを有効にしていきます。

multi_db.rb
Rails.application.configure do
  config.active_record.database_selector = { delay: 2.seconds }
  config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
  config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end

これによって、witerDBとreplicaDBの自動切り替えを有効にするとができました。
しかし、切り替えの条件で説明したようにこの自動切り替えは柔軟な対応をすることができません。
例えば、GETリクエストでデータの取得・読み取りをしたいが、取得・読みとるデータはリアルタイム性が重視される場合、replicaDBからデータを取得するのは理にかなっていません。

そこで、下記の章ではwiterDBとreplicaDBの手動切り替えについて説明します。
この手動切り替えの方が柔軟な対応ができるので実務ではよく使われるのではないでしょうか?
それでは見ていきましょう。

witerDBとreplicaDBの手動切り替え

ここでは、先ほど説明したようにwiterDBとreplicaDBの手動切り替えについて見ていきます。
手動で必要なコネクションに切り替えるにはconnected_toメソッドを使用します。

ActiveRecord::Base.connected_to(role: :reading) do
  # このブロック内のコードはすべてreadingロールで接続される
end

connected_toメソッド内にrole: :readingの設定を記述するとreading専用のreplicaDBに接続することを意味します。そして、反対にrole: :writing の設定を記述するとwiterDBへの接続を意味します。
このconncted_toメソッドは、ブロック形式で実行されブロック内だけで指定したデータベースが使用されます。利用例としては、下記のようなものがあると思います。

# 通常の操作はプライマリDB(書き込み用)
User.create(name: "John")

# レプリカDBに接続してデータを読み取る
ActiveRecord::Base.connected_to(role: :reading) do
  user = User.find_by(name: "John") # レプリカから読み取る
end

# ここに戻るとまたプライマリDBが使用される

このようにconnected_toメソッドを使用することでブロック単位で柔軟にアクセスするDBを切り替えることができるようになります。

最後に

今回の記事では、Railsの他DBの接続設定やreplicaDBを使った負荷分散、readonlyの設定などを見てきました。Railsはシステム開発の0→1を得意としていますが、技術的負債などが溜まりやすいフレームワークだと思っています。
技術的負債が溜まってくるとどうしても速度改善や負荷分散が難しくなってきます。
そのようになる前に設計段階で今回学んだようなreplicaDBをうまく使う計画やreadonlyを使ってスコープをしっかり分ける設計などをしていきましょう!

Discussion