【増補改訂版】パーフェクト Ruby on Rails を読む - その3

【増補改訂版】パーフェクト Ruby on Rails を読んだので、頭が整理できた部分をなるべく自分の言葉でまとめていきます。

第1章、第2章の内容については以下を御覧ください。

【増補改訂版】パーフェクト Ruby on Rails を読む - その1 - readengineerのブログ

第3章の内容については以下を御覧ください。

【増補改訂版】パーフェクト Ruby on Rails を読む - その2 - readengineerのブログ

5 章

Rails 標準の機能を活用して素早く機能実装する

Rails は「利用頻度は高くないが、0 から実装しようとすると時間がかかるもの」を標準機能として提供している。

Active Job による非同期実行
非同期処理実行処理機能を提供するもの。実行処理を別のインフラに任せることができる。インフラは多様なものを選べるが、その処理内容の定義を統一化してくれるもの。

bin/rails g job ジョブ名 のようなスクリプトを実行すると、ジョブを定義するファイルが app/jobs/* に生成される。

class TestJob < ApplicationJob
  queue_as :default

  def perform(*args) # 引数は変更できる。例えば def perform(name) とか
    # Do something later 
    puts 'hello' # ここではモデルの操作なんかもできる。
  end
end

rails console を起動して、以下のように実行してみることができる。
アダプターというものを介してバックエンドと接続・切替をしており、デフォルトのアダプタは async というもので、Rails のプロセス中のスレッドを利用して実行するもの。

perform_later メソッドはバックエンドのキューに積んでくれる。
async を使うとメモリ内にキューが保管されているため、再起動するとキューが消えてしまう。

irb(main):002:0> TestJob.perform_later
Enqueued TestJob (Job ID: 9e2dc5d2-7289-4e33-a907-4f09e61dd80b) to Async(default)
=> 
#<TestJob:0x00007fa492970cc8
 @arguments=[],
 @exception_executions={},
 @executions=0,
 @job_id="9e2dc5d2-7289-4e33-a907-4f09e61dd80b",
 @provider_job_id="f5a5625e-0f71-4372-90c5-b7d8aae8f4c8",
 @queue_name="default",
 @timezone="UTC">
irb(main):003:0> Performing TestJob (Job ID: 9e2dc5d2-7289-4e33-a907-4f09e61dd80b) from Async(default) enqueued at 2021-12-14T00:23:55Z
hello
Performed TestJob (Job ID: 9e2dc5d2-7289-4e33-a907-4f09e61dd80b) from Async(default) in 0.07ms

実行日時の設定や、メソッドが呼ばれてから実行するまでの待機時間の設定、同期的な実行方法など、多様な設定が可能。

キューを永続化したいときは、Redis を利用したジョブキュー管理を行ってくれる Sidekiq や resque などの gem を使うことになる。

注意点としては、Active Job を利用して基本的な機能を利用することができるが、一部でバックエンドの機能が利用できない場合があるらしい。例えば sidekiq pro などで提供している機能とか。
バックエンドを差替える必要が出てくる場合や、Active Job に依存している場合などは Active Job を利用する必要性があるが、できないことがあることも知っておく必要がある。

【増補改訂版】パーフェクト Ruby on Rails を読む - その2

【増補改訂版】パーフェクト Ruby on Rails を読んだので、頭が整理できた部分をなるべく自分の言葉でまとめていきます。

第1章、第2章の内容については以下を御覧ください。

【増補改訂版】パーフェクト Ruby on Rails を読む - その1 - readengineerのブログ

3 章 抑えておきたい Rails の基本機能

テストの種類と実行方法

Rails 6.0 で追加された並列テスト

Rails 6.0 では、デフォルトでテストが並列化される設定となっている。

並列度の指定は test/test_helper.rb に記述されている。

ENV['RAILS_ENV'] ||= 'test'
require_relative "../config/environment"
require "rails/test_help"

class ActiveSupport::TestCase

class ActiveSupport::TestCase
  parallelize_setup do |worker|
    # データベースをセットアップする
  end

  parallelize_teardown do |worker|
    # データベースをクリーンアップする
  end

  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors) # <-- ここ

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

たとえば、以下のように 5 秒間待機するテストを 4 つ実行するとします。 並列なしの場合はテスト完了までに 20 秒以上かかります。

require 'test_helper'

class TaskTest < ActiveSupport::TestCase
  test 'the truth' do
    assert true
  end

  test 'sleep 1' do
    sleep 5
  end

  test 'sleep 2' do
    sleep 5
  end

  test 'sleep 3' do
    sleep 5
  end

  test 'sleep 4' do
    sleep 5
  end
end

しかし実行すると、5.450333s でかんりょうしていました。 4 つのテストが並列で実行されていた事がわかります。

$ bin/rails test
Running via Spring preloader in process 41509
Run options: --seed 41044

# Running:

...........

Finished in 5.450333s, 2.0182 runs/s, 1.6513 assertions/s.
11 runs, 9 assertions, 0 failures, 0 errors, 0 skips

次に並列数を 1 に設定して、同じテストを実行してみます。

ENV['RAILS_ENV'] ||= 'test'
require_relative "../config/environment"
require "rails/test_help"

class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: 1) # <-- ここを 1 にした

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

すると、やはりテスト完了までに 20 秒以上かかりました。

$ bin/rails test
Running via Spring preloader in process 45358
Run options: --seed 54407

# Running:

...........

Finished in 20.370227s, 0.5400 runs/s, 0.4418 assertions/s.
11 runs, 9 assertions, 0 failures, 0 errors, 0 skips

並列化は2種類の方法があって、「プロセス(デフォ)」または「スレッド」を利用した並列を選択可能。 プロセスはメモリを共有せず、スレッドはメモリを共有する。らしい。

そのため、プロセスを利用して並列化されたテストでは、テスト DB の読み込みについてはプロセスごとに扱うため、設定が必要。

そして 2021 年 11 月時点では RSpec では並列テストができないよう。

Rack と Rails の関係

Rack とは

Ruby の Web アプリフレームワークRailsSinatra など)
    ↕
インターフェース:Rack
    ↕
Web アプリケーションサーバUnicorn や Puma など)

Rack 登場以前は、アプリケーションサーバフレームワークが密結合でデプロイが大変だった。 Python も同じ悩みを抱えていて、WSGI という Web アプリケーション/フレームワーク間のインターフェースの規格が決めた。それに乗っかって Ruby 用に開発されたのが Rack 。

Rack の基本

# 規約:call というメソッドを定義して、env か environment という名前の引数を1つ受取るのが慣例
def call(env)  
  status = 200
  headers = { "Content-Type" => "application/json" }
  bodt = { "hoge": "fuga" }

  # 規約:返り値は http のステータスコード、ヘッダー、レスポンスボディとなる文字列を含んだ配列風オブジェクト
  [status, headers, body]
end

上記の call メソッドの形式で定義されたアプリケーションやミドルウェアが、その中で次の処理を行うアプリケーションやミドルウェアを呼び出すという、入れ子状態になっている。(以下のような玉ねぎで例えられる)

f:id:skgch0702:20211201203910p:plain
Python WSGIフレームワークPylons」のドキュメントより https://docs.pylonsproject.org/projects/pylons-webframework/en/v1.0.1rc1/concepts.html

クライアントからリクエストを受けると、順番に call メソッドが呼ばれていき、中心に位置する Rack アプリケーションに到達すると、返り値がどんどん返されていき、クライアントへレスポンスを返す。

Rails と Rack の関係

Rails は上記の Rack アプリケーションに、独自のミドルウェアを追加したもの。 ターミナルで bin/rails middleware というコマンドを打つと、搭載されているミドルウェアが、搭載されている順番に表示されるようになっている。

$bin/rails middleware

use Webpacker::DevServerProxy
use Rack::MiniProfiler
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run RailsTest::Application.routes

ミドルウェアの前後に独自の処理を行いたい場合は、自作のミドルウェアも作って任意の位置に仕込むことが可能。 具体的な用途はわかっていない。

DB を管理する

複数の DB を扱う

大規模な Web アプリケーションになると DB へのアクセスが応答速度低下のネックになりやすい。 その場合の対処として、DB を複数用意して負荷分散をするケースがある。

これまでは gem(Octopus など) を使って複数 DB の利用を可能にしていたが、Rails6 からは標準機能で利用できるようになった。

Rails ガイドによると、現状対応している機能は以下の通り。

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

引用:Active Record で複数のデータベース利用 - Railsガイド

ここで、4つ目は rails db:setup のようなコマンドが使えるよ、という意味。 その他の3つが主要な機能になる。

  • 複数の DB を用意して、モデルごとに振り分ける

まず DB を複数用意するには、
db/sub_migrate というディレクトリを作成し、 config/database.yaml を以下のように書き換える。

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  primary:
    <<: *default
    database: db/development.sqlite3
  sub:
    <<: *default
    database: db/development_sub.sqlite3
    migration_paths: db/sub_migrate

省略

この時点で rails db:create をするとそれぞれの DB が作成される。
rails db:create:sub とかすると、sub の DB だけ作成されたりする。他のコマンドも同様。

sub に登録したいモデルに対して、接続先を指定する必要がある。
まずモデルを作成する際には、以下のように保存先 DB を指定することが可能。

bin/rails g model Publisher name:string --database=sub

さらに以下のように Application Record を継承する基底クラスを作成し、モデルがそれを継承する様に定義する。(もちろん各モデルで以下の establish_connection を定義しても OK )

# 新たに手動作成したファイル。例えば models/sub_base.rb など
class SubBase < ApplicationRecord
  self.abstract_class = true

  establish_connection :sub
end
# 新たに bin/rails g model で作成したモデル。ここでは Publisher モデル
class Publisher < SubBase
end

以上で設定は終了。
あとは Publisher.create(name: "hoge") のようにレコードを作成すると、設定通り sub の DB に保存してくれる。

  • 書き込み先に primary 、読み込み元に replica を指定する

実際の需要が多いのはこちら。
続いては DB の replica を用意する。

その場合は以下のように config/database.yml を編集する。( mysql の例)

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  username: root
  password: 
  host: 127.0.0.1

development:
  primary:
    <<: *default
    database: db/development.sqlite3
    port: 33061
  primary_replica:
    <<: *default
    database: db/development_sub.sqlite3
    port: 33062
    replica: true  # <--- これで `rails db:xxx` のコマンドが反映されるようになる

省略

それぞれの操作で接続先を明示する必要がある。 すべてのモデルに反映する場合は、app/models/application_record.rb で以下のように設定する。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: {
    writing: primary,
    reading: primary_replica
  }
end

【増補改訂版】パーフェクト Ruby on Rails を読む - その1

【増補改訂版】パーフェクト Ruby on Rails を読んだので、頭が整理できた部分をなるべく自分の言葉でまとめていきます。

1 章 Ruby on Rails の概要

Rails の思想

Rails は4つの基本思想からなる。

1. CoC (Convention over Configuration)

直訳は「設定より規約」

Rails の規約を守ると 手動の設定が減り共有ルールにより会話が円滑 になる。
それによって サービスのコアバリューの実現に集中できる というメリットが有る。

[規約]

  • DB のテーブル名はモデル名の複数形にする( Employee モデルの時 employees )
  • /employees という URL は社員の一覧を表す
  • /employees/1 は 社員 ID が1の社員情報を表す


2. DRY (Don't Repeat Yourself)

直訳は「同じことを繰り返さない」。

同じ処理やコードは1箇所にまとめることで、メンテナンス性が向上する。

Rails 起動時にモデルに対応するテーブルのカラム名一覧を取得することで、employee.name のようなメソッドなどを利用できるようにしている。


3. REST (Representational State Transfer)

これは Web アプリケーションの設計概念の1つ。 その概念のうち、以下の考えを取り入れている。

  • すべてのリソースに一意となる識別子がある(例:/employees/1 など)
  • URI を通してリソースを操作する手段を提供

これにより、簡潔で拡張しやすいコード になる。


4. 自動テスト

設定しなくてもテストのテンプレートが作成され、テストを実行する環境が揃っている。

2 章 Ruby on RailsMVC

MVC アーキテクチャ

改めて M, V, C の説明。

  • M = Model:DB との接続と、データの操作およびビジネスロジック
  • V = View:Model の内容を参照し 表現する。
  • C = Controller:Model ロジックを呼出し View の選択をする、など M と V をつなぐ。

「システムを作る」ということは「解決したい問題に関する概念をデータ化して名前や関連を定義する」こと。 この行為をモデリングともいう。

MVC のモデルは、モデリングのモデルとほぼ同義 である。

そして、Rails では Active Record によってモデルを定義し、以下の役割を持つ。

1. DB と接続して、DB のレコードと Active Record オブジェクトを結びつける。

[主な機能の例]

  • DB のカラム情報をインスタンス生成時のフィールド情報に反映する
  • SQL の構築を抽象化する
  • DB のコネクションプールや接続の情報を隠蔽する


2. ビジネスロジックの振る舞いを実行する。

バリデーションやレコード保存や削除等の際に実行されるコールバックを実行する ※ 詳細は 12 章

モデルを扱う

ActiveRecord::Relation クラス
DB のレコードを取扱う際に、ActiveRecord::Relation というクラスを使う。
具体的には、ActiveRecord の Query Interface によって操作した結果を ActiveRecord オブジェクトとして表現する。

例えば Book モデルが存在している場合に、Book.where('pages > ?', 300) のように where メソッドを呼び出すと「 where メソッドに対応する SQL を発行する」という情報を持つ。
このとき 実行結果が必要となるタイミングで DB にアクセスする
そして実際に発行されるまでは、繰返しメソッドが呼び出されれば SQL が更新(追加)されていく。
取得された結果は ActiveRecord オブジェクトの配列のような形で使える。

Scope

繰り返し利用するクエリは app/models/ にあるモデルのファイルで scope として定義すると簡単に利用できる。(引数も渡せる)

コントローラーの役割

ブラウザからリクエストがあったときに、ルーティングに登録されたコントローラのアクションが、データの取得・加工、返却(ビューに描画指示)を行う。

フック
アクションに対して処理を設定するフックは以下の3つが存在する。

  • before_action: アクション実行前
  • after_action: アクション実行後
  • around_action: アクション実行前後

around_action は以下のように yield でアクションを実行する部分を指定する。

class HogeController < ApplicationController
  around_action :logger_around_action, only: [:destroy] 
  # 略
  private
  # 略
  def logger_around_action
    logger.info("before action")
    yield
    logger.info("after action")
  end
end

CSRF 対策

CSRF とは悪い人が用意したリンクをクリックすると、勝手にデータ操作されてしまうという攻撃のこと。 Rails ではセキュリティートークンを用意して、GET リクエスト以外はトークンを検証して、正しいアクセスかを確かめている。 protect_from_forgery というクラスメソッドを使うと、トークンによる検証をスキップできる。