パーフェクトRuby on Rails 3章 メモ・雑感(Rack, 複数DBの取り扱い)

# 3章 押さえておきたいRailsの基本機能

## 雑感

この章ではテストやミドルウェア、データベースを取り扱う基本機能が説明されていました。基本機能といってもRackや複数DBなど、初見の知識がこの章から多くなってきた印象です。特に複数DBのユースケースはかなり実務に近い内容と感じたのでしっかりと押さえておきたいと感じました。以下、個人的な要所をまとめた忘備録です。

## 3-2 RackとRailsの関係

### RackアプリケーションとしてのRails

- RailsはUnicornやPumaなど様々なWebアプリケーションサーバで上で動作するが、これはRailsがRackインターフェースに則っているため
- Rackはアプリケーションサーバとフレームワークの中間に存在し、この中間層を経ることで疎結合なやり取りが行える
- Rackが利用するエントリーポイントとして`config.ru`ファイルが存在し、`rails new`すると生成される

```config.ru
# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application # runメソッドにRailsオブジェクトを渡して実行している
```

- railsをRackアプリケーションとして起動させる場合は、`rails s`ではなく`rack up`コマンドを実行すると9292番ポートで立ち上がる
```
$ bundle exec rackup
Puma starting in single mode...
* Version 4.3.12 (ruby 2.7.8-p225), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop
```

### Rackミドルウェアでサーバとアプリケーションの間に処理を追加する

- Railsは初期状態で様々な中間処理を行うミドルウェアを利用している。`use`の右がミドルウェア名

```
$ bin/rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
.
.
.
use Rack::ETag
use UpcaseMiddleware
use Rack::TempfileReaper
run RailsRackSample::Application.routes
```

- 自作のRackミドルウェアをRailsに追加して任意の中間処理を設定することもできる。ファイルを作成後に、`config/environments`内のファイルに設定を追加する

```lib/middlewares/upcase_middleware.rb
# rubyを大文字へ変換するミドルウェア
class UpcaseMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body.each { |s| s.gsub!(/ruby/i, 'RUBY') }
    [status, headers, body]
  end
end
```

```diff_ruby:config/environments/development.rb
require "middlewares/upcase_middleware"
Rails.application.configure do
  .
  .
  .
-    # 通常は全ての処理の最後に追加
-    config.middleware.use UpcaseMiddleware
+    # 指定した別ミドルウェアの後に処理を追加する事もできる
+    config.middleware.insert_after Rack::ETag, UpcaseMiddleware
end
```

## 3-3 DBを管理する

### 複数DBの扱い方

- 複数のDBを取り扱う場合、`config/database.yml`へ他のDB設定を追加する

```diff_yaml:config/database.yml
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  primary:
    <<: *default
    database: db/development.sqlite3
+ sub: # サブDBを追加
+   <<: *default
+   database: db/development_sub.sqlite3 # サブDBの名前
+   migrations_paths: db/sub_migrate # migrateした場合にファイルが追加されるパス
test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3
```

- 片方のDBにマイグレーションパスを指定することで２つのDBで異なるテーブル定義を反映する
- `db:create:sub`のようにタスクに利用するDB名を記述することで特定のDBのみをタスクの対象とできる

```
$ bin/rails db:create:sub # <-- subだけdb:createされる
Created database 'db/development_sub.sqlite3'
```

- 各モデルクラスで`establish_connection`を設定しsubデータベースへ接続する。その際、役割を分割するためにDB接続設定を定義した抽象クラスを作成してそのクラスを継承すると良い。設定後は自動的にsubへ書き込みなどが行われる

```app/models/sub_base.rb
# DB接続設定を定義する抽象クラス
class SubBase < ApplicationRecord
  self.abstract_class = true
  establish_connection :sub
end
```

```app/models/author.rb
# 上記の抽象クラスを継承してsubデータベースへ接続する
class Author < SubBase
end
```

### 書き込みと読み込みの分離

- 実際のユースケースでは書き込み用と読み取り用のDBに分割する構成が多い。この場合、読み取り専用のレプリカDBを作成するときは、`database.yml`へ`replica`を指定する

```diff_yaml:config/database.yml
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: 
  host: 127.0.0.1

development:
  primary:
    <<: *default
    database: db_sample_development
    port: 33061
+   primary_replica:
+   <<: *default
+   database: db_sample_development
+   port: 33061
+   replica: true # 読み取り専用であることを宣言
test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3
```

- 書き込みや読み込み先のDBを明示的に指定するには`ActiveRecord`モデル単位で`connect_to`メソッドを利用する

```app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: {
  # ロール名   DB名
    writing: :primary,
    reading: :primary_replica
  }
end
```

- ロール名は`application.rb`などで変更も可能

```config/application.rb
module DbSample
  略
  class Application < Rails::Application
    config.active_record.writing_role = :writable
    config.active_record.reading_role = :readonly
  end
end
```

- 特定のクエリでレプリカを参照したい場合は、`connect_to`にロールを指定したブロックを使う

```rb
ActiveRecord::Base.connect_to(role: :reading) do
  Blog.find(5)
end
```

- 多くの場合は読み取りであれば自動的にレプリカDBを参照してほしいケースが多いので、そのために必要な設定が`production.rb`にコメントアウトされている。コメントアウトを解除し、`development.rb`へそのままコピーすることで開発環境でも有効にできる

```config/environments/development.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
```