ActiveResourceとマルチDBのどちらを使うべきか?


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のように扱うことが可能になります。

![](https://outsource-systems.com/content/images/2021/04/about-active_resource.png)

## ActiveResourceの使い方

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

[https://github.com/rails/activeresource](https://github.com/rails/activeresource)

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

前提として、

- アプリAには、`ActiveResource::Base`を継承したTaskクラスを作成しておきます。（ControllerやRouteの定義は不要）

```ruby:app/models/task.rb
class Task < ActiveResource::Base
end
```

- アプリBにはTaskというモデルを`Scaffold`で生成し、ローカルホスト3001番で起動している状態です。

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

[https://railsguides.jp/command_line.html#rails-generate](https://railsguides.jp/command_line.html#rails-generate)

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

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

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

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

```shell
$ 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)`などとして、リソースの取得が可能ですが、

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :tasks
end
```

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

```ruby:config/routes.rb
Rails.application.routes.draw do
  namespace "api" do
    namespace "v1" do
      resources :tasks
    end
  end
end
```

```ruby:app/models/task.rb
class Task < ActiveResource::Base
  self.site = 'http://localhost:3001'
  self.prefix = '/api/v1/'
end
```

```shell
# 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モデルに以下を追加します。


<!-- TODO: アプリBに記述する理由 -->
```ruby
class Task < ActiveRecord::Base
  validates :title, presence: true
end
```

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

```shell
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
- [https://github.com/rails/activeresource](https://github.com/rails/activeresource)

- [http://webos-goodies.jp/archives/how_to_use_activeresource_1.html](http://webos-goodies.jp/archives/how_to_use_activeresource_1.html)

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

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

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

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

![](https://outsource-systems.com/content/images/2021/04/about-multiple-db.png)

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

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

[RubyonRailsガイドより抜粋](https://railsguides.jp/active_record_multiple_databases.html)より抜粋

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

### 複数DBを使ってみる

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

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

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

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

```yml:config/database.yml
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に関しては抽象クラスを作成する必要があります。

```ruby
# 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`でコマンドを確認してみましょう

```shell
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ファイルを作成しています。

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

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

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

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

```shell
$ bundle exec rails db:migrate:secondary
```

### モデルファイルの生成

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

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

### 複数DBの挙動の確認

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

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

```shell
# 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の負荷分散の仕組みも追加されました。

![](https://outsource-systems.com/content/images/2021/04/about-primary-replica.png)

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

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

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

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

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

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

```ruby
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`をインストールし、設定をします。

```ruby
# 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`を、追加します。

```ruby
Rails.application.routes.draw do
  resources :admins
end
```

```ruby
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`

```shell
Admin Load [reading](3.4ms)
SELECT
    `admins`.*
FROM
    `admins`
```

- リソースの作成

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

```shell
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`

```shell
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`

```shell
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`

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

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

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

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

### デメリット
- DBを跨いだ テーブル同士のJOINができない。


## 記事引用元
https://outsource-systems.com/activeresourcetorails6yi-jiang-fu-shu-dbjie-sok/