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タスク
尚、DBを跨いでのテーブルのJOINや、一つテーブルを複数のDBのテーブルに分割して負荷分散させるシャーディング
などはまだサポートされていません。
複数DBを使ってみる
複数DBを使用できるようにするには、はじめにdatabase.yml
を編集します。
primaryとreplicaは同じデータを持つため、同じdatabase名にします。
replicaのDBに、replica: true
を指定してあげることでRails側でreplicaを認識してもらいます。
migration_path
を指定して、primary_database
とsecondary_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
実際にリクエストを送信して、レスポンスが確認できるように、routing
とcontroller
を、追加します。
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ができない。
記事引用元