ActiveResourceとrails6以降複数DB接続機能のメリットとデメリットをご紹介していきます。

どちらを使うべきかはそれぞれのメリットデメリットにみて、ご自身で検討してください。

  • ActiveResourceのとは
  • ActiveResourceの使い方
    • Rails間でリソースのCRUD操作をする
    • Prefix(接頭辞)を指定する
    • Validationを設定する
  • ActiveResourceのメリット・デメリット
    • メリット
    • デメリット
  • Rails6で追加された複数DB接続
    • 複数DBを使ってみる
    • どのように複数DBへ接続するのか
  • primary/replica
    • コネクションの自動切り替え
    • primary/replicaの挙動の確認
  • 複数DB接続できるようになったメリット・デメリット
    • メリット
    • デメリット

ActiveResourceとは

ActiveResourceは、Rails2.0〜3.x に搭載されていた機能です。
Rails4.0 からは搭載されていませんが、Gemを追加することで利用
可能です。

複数のRailsアプリを繋ぐことができ、互いのモデルをActiveRecordのように扱うことが可能になります。

ActiveResourceの使い方

ActiveResourceを始めるための準備はとても簡単です。
gemである、activeresourceを追加するだけで利用が可能になります。

https://github.com/rails/activeresource

アプリAとアプリBがあり、アプリAからアプリBのリソースを取得する実装を例にとって解説します。

前提として、

  • アプリAには、ActiveResource::Baseを継承したTaskクラスを作成しておきます。(ControllerやRouteの定義は不要)
class Task < ActiveResource::Base
end
  • アプリBにはTaskというモデルをScaffoldで生成し、ローカルホスト3001番で起動している状態です。

Scaffoldについての詳細はこちら

https://railsguides.jp/command_line.html#rails-generate

Rails間でリソースのCRUD操作をする

アプリAからアプリBに接続してリソースの取得や更新などを実行したい場合は、アプリAのTaskクラスに以下のよう記述を追加します。

class Task < ActiveResource::Base
  self.site = 'http://localhost:3001'
end

設定が完了したら、通常のActiveRecordのようリソースの作成、取得、更新、削除といったCRUD操作が可能になります。

$ bundle exec rails c
# リソースの生成
> Task.create(title: "作成")
# インスタンスを生成して保存
> task = Task.new(title: "作成")
> task.save
# リソースの取得
> Task.all
# 単一リソースの取得
> Task.find(n)
# 単一リソースの更新
> Task.find(n).update_attributes(title: "更新")
# 単一リソースの削除
> Task.find(n).destroy

Prefix(接頭辞)を指定する

以下のようなルーティングを定義している場合、通常通り、モデル名.find(n)などとして、リソースの取得が可能ですが、

Rails.application.routes.draw do
  resources :tasks
end

namespaceなどが入ってたりする場合には、取得する側(アプリA)でPrefixを指定する必要があります。

Rails.application.routes.draw do
  namespace "api" do
    namespace "v1" do
      resources :tasks
    end
  end
end
class Task < ActiveResource::Base
  self.site = 'http://localhost:3001'
  self.prefix = '/api/v1/'
end
# Task.idが 1 のデータを取得
> Task.find(1)
> #<Task:0x00007fc2a8d01ca0 @attributes={"id"=>1, "title"=>"こんちわ!", "description"=>"ほげほげ", "is_done"=>true, "created_at"=>"2021-03-25T08:49:31.627Z", "updated_at"=>"2021-03-25T08:49:31.627Z", "url"=>"http://localhost:3001/api/v1/tasks/1.json"}

返ってきたデータのurlをみてみると

"url"=>"http://localhost:3001/api/v1/tasks/1.json"

/api/v1/といったprefixが指定されています。

Validationを設定する

Validationも通常のRailsアプリケーション同様に使うことができます。
Taskのtitleが空であるかの検証を追加します。空であればエラーメッセージが返ります
。アプリBのTaskモデルに以下を追加します。

class Task < ActiveRecord::Base
  validates :title, presence: true
end

アプリAのrails consoleからtitleが空のインスタンスを生成して保存してみます。

irb(main) > task = Task.new(title: "")
irb(main) > task.save
=> false
irb(main) > task.errors.full_messages
=> ["title can't be blank"]

ActiveResourceのメリット・デメリット

メリット

  • 複数のRailsアプリケーションが疎結合になり、microserviceのようなアーキテクチャを構築することができ、サービス自体をスケールしやすい。

デメリット

  • 別アプリのDBからAPIに過剰にアクセスするため、パフォーマンスが落ちる。

参考記事URL

Rails6から追加された複数DB接続

アプリケーションの規模が大きくなってくると、データの数が膨大になり、DBのパフォーマンスも低下してしまいます。

そういった場合にDBをスケールさせる必要が出て来ますが、Rails6以前では複数DBが標準サポートがなされていなかったため、設定をするのが少し大変であったり、ActiveResourceといった機能を利用したりしていました。

Rails6からは複数DBを標準サポートするようになったため、DBのスケールさせることが容易になりました。

この記事を執筆している2021年4月の段階では、複数DB機能として、具体的なサポート内容は以下の4つのようです。

  • 複数の「primary」データベースと、それぞれに対応する1つの「replica」
  • モデルでのコネクション自動切り替え
  • HTTP verbや直近の書き込みに応じたprimaryとreplicaの自動スワップ
  • マルチプルデータベースの作成、削除、マイグレーション、やりとりを行うRailsタスク

RubyonRailsガイドより抜粋より抜粋

尚、DBを跨いでのテーブルのJOINや、一つテーブルを複数のDBのテーブルに分割して負荷分散させるシャーディングなどはまだサポートされていません。

複数DBを使ってみる

複数DBを使用できるようにするには、はじめにdatabase.ymlを編集します。

primaryとreplicaは同じデータを持つため、同じdatabase名にします。

replicaのDBに、replica: trueを指定してあげることでRails側でreplicaを認識してもらいます。

migration_pathを指定して、primary_databasesecondary_databaseのmigrationファイルのパスを分けます。

development:
  primary:
    <<: *default
    database: primary_database
  primary_replica:
    <<: *default
    database: primary_database
    replica: true
  secondary:
    <<: *default
    database: secondary_database
    migrations_paths: db/secondary_migrate
  secondary_replica:
    <<: *default
    database: secondary_database
    replica: true

それぞれDBへ接続するための設定を追加します。
secondaryに関しては抽象クラスを作成する必要があります。

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  # 書き込み、読み込み先の設定
  connects_to database: { writing: :primary, reading: :primary_replica }
end

# app/models/secondary_base.rb
class SecondaryBase < ApplicationRecord
  self.abstruct_class = true

  # 書き込み、読み込み先の設定
  connects_to database: { writing: :secondary, reading: :secondary_replica }
end

これで複数DBを扱う準備が整ったので、bundle exec rails -Tでコマンドを確認してみましょう

rails db:create                          # Creates the database from DATABASE_URL or config/database.yml for the current RAI...
rails db:create:primary                  # Create primary database for current environment
rails db:create:secondary                # Create secondary database for current environment
rails db:drop                            # Drops the database from DATABASE_URL or config/database.yml for the current RAILS...
rails db:drop:primary                    # Drop primary database for current environment
rails db:drop:secondary

primary_databaseとsecondary_databaseに対して操作できるコマンドが増えていると思います。

migrationファイルの生成

次にmigrationの設定をします。以下コマンドではsecondary_databaseのmigrationファイルを作成しています。

$ bundle exec rails g migration CreateAdmin name:string email:string --database secondary

--databaseオプションでどのDBのマイグレーションファイルが作成するのかを指定します。

実行ができたら、database.ymlにて指定した、migration_pathsのディレクトリが生成され、ディレクトリ配下に先ほど生成したmigrationファイルが配置されます。

migrationを実行する際にも、databaseを指定します。

$ bundle exec rails db:migrate:secondary

モデルファイルの生成

次にモデルファイル作成します。
Adminはsecondary_databaseからデータの読み書きを行うので、継承元をSecondaryBaseとします。

# class Admin < ApplicationRecord
class Admin < SecondaryBase # こちらに変更
end

複数DBの挙動の確認

実際にデータを追加してみて意図したDBへ登録されているか確認します。

Adminはsecondary_databaseに登録されるはずです。

# secondaryDBにデータを投入
> Admin.create(name: "sample", email: "sample@email.com")

# primaryDBを確認する
mysql > use primary_database;
mysql > SELECT * FROM admins;
ERROR XXX: Table 'primary_database.admins'....

# secondaryDBを確認する
mysql > use secondary_database;
mysql > SELECT * FROM admins:
+----+-------+------------------+----------------------------+----------------------------+
| id | name  | email            | created_at                 | updated_at                 |
+----+-------+------------------+----------------------------+----------------------------+
|  1 | admin | sample@email.com | 2021-03-26 11:54:32.952708 | 2021-03-26 11:54:32.952708 |
+----+-------+------------------+----------------------------+----------------------------+
1 row in set (0.00 sec)

これで正しいDBへ登録されていることが確認できました。

primary/replica

Rails6からは複数DBに加えて、primary/replicaというDBの負荷分散の仕組みも追加されました。

GETなど読み込み専用のDBとして replica(複製)へ、POST,PUT,PATCH,DELETEなどDBへの書き込みが伴うリクエストは primary(書込)へ接続します。

Railsはこのprimary、replicaをリクエストに応じて自動で切り替えることができます。

コネクションの自動切り替え

primary/replicaを自動で切り替えられるように設定します。

先述したように、primaryは書き込み専用(POST,PUT,PACTH,DELETE)であり、replicaは読み込み専用(GET)とするため、リクエストに応じて、接続先を変更する必要があります。

以下をapplication.rbに追加します。

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

primary/replicaの挙動の確認

次にリクエストに応じてprimary/replicaが切り替わるか確認します。

クエリのログにDBの接続状況を出力させたいのでgem arproxyをインストールし、設定をします。

# config/initialize/arproxy.rb
if Rails.env.development? || Rails.env.test?
  require ‘mulitiple_database_connection_logger’
  Arproxy.configure do |config|
    config.adapter = ‘mysql2’
    config.use MultipleDatabaseConnectionLogger
  end
  Arproxy.enable!
end

# lib/multiple_database_connection.rb
class MultipleDatabaseConnectionLogger < Arproxy::Base
  def execute(sql, name = nil)
   role = ActiveRecord::Base.current_role
   name = “#{name} [#{role}]”
   super(sql, name)
  end
 end

実際にリクエストを送信して、レスポンスが確認できるように、routingcontrollerを、追加します。

Rails.application.routes.draw do
  resources :admins
end
class AdminsController < ApplicationController
  protect_from_forgery with: :null_session
  before_action :set_admin, only: %i[show update destroy]

  def index
    admins = Admin.all
    render json: { sataus: :ok, data: admins }
  end

  def show
    render json: { status: :ok, data: @admin }
  end

  def create
    @admin = Admin.create!(controller_params)
    render json: { status: :ok, data: @admin }
  end

  def update
    @admin.update(controller_params)
    render json: { status: :ok, data: @admin }
  end

  def destroy
    @admin.destroy
    render json: { status: :ok, message: "deleted admin" }
  end

  private

  def controller_params
    params.require(:admin).permit(:name, :email)
  end

  def id
    params[:id]
  end

  def set_admin
    @admin = Admin.find(id)
  end
end

ここまでできたら、curlコマンドでリクエストを送信して、read-writeが切り替わっているか確認してみます。

取得などのリクエストの場合は[reading]、書き込みや更新、削除リクエストの場合は[writing]になっているのがわかると思います。

  • リソースの一覧を取得

curl -X GET localhost:3000/admins

Admin Load [reading](3.4ms)
SELECT
    `admins`.*
FROM
    `admins`
  • リソースの作成

curl -X POST -d "admin[name]=hoge, admin[email]=hogehoge@email.com" localhost:3000/admins

Admin Create [writing](
    14.8ms
)
INSERT INTO `admins`(
    `name`,
    `created_at`,
    `updated_at`
)
VALUES(
    'hoge, admin[email]=hogehoge@email.com',
    'YYYY-MM-DD 09:23:59.939996',
    'YYYY-MM-DD 09:23:59.939996'
)
  • 単一リソースの取得

curl localhost:3000/admins/1

Admin Load [reading](1.9ms)
SELECT
    `admins`.*
FROM
    `admins`
WHERE
    `admins`.`id` = 1
LIMIT 1
  • リソースの更新

curl -X PUT -d “admin[name]=hoge” localhost:3000/admins/1

Admin Update [writing](24.8ms)
UPDATE
    `admins`
SET
    `admins`.`name` = 'hoge',
    `admins`.`updated_at` = 'YYYY-MM-DD 09:25:58.924513'
WHERE
    `admins`.`id` = 1
  • リソースの削除

curl -X DELETE localhost:3000/1

Admin Destroy [writing](6.8ms)
DELETE
FROM
    `admins`
WHERE
    `admins`.`id` = 1

複数DBのメリットデメリット

メリット

  • 一つのアプリケーションで膨大なデータを扱えるようになることや、primary-replicaを活用し、DBの仕事を分散させることで、DB自体のパフォーマンスを保ちやすい。

  • activeresourceを使う必要がないので、実装コストを抑えることができ、httpリクエストをしないので、サーバーのパフォーマンスを抑えることができる。

デメリット

  • DBを跨いだ テーブル同士のJOINができない。