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
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設定を追加する
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にマイグレーションパスを指定することで2つの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
を指定する
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
にロールを指定したブロックを使う
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