【増補改訂版】パーフェクト 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 アプリフレームワーク( Rails や Sinatra など)
↕
インターフェース: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 メソッドの形式で定義されたアプリケーションやミドルウェアが、その中で次の処理を行うアプリケーションやミドルウェアを呼び出すという、入れ子状態になっている。(以下のような玉ねぎで例えられる)
クライアントからリクエストを受けると、順番に 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 Rails と MVC
MVC アーキテクチャ
改めて M, V, C の説明。
- M = Model:DB との接続と、データの操作およびビジネスロジック。
- V = View:Model の内容を参照し 表現する。
- C = Controller:Model ロジックを呼出し View の選択をする、など M と V をつなぐ。
「システムを作る」ということは「解決したい問題に関する概念をデータ化して名前や関連を定義する」こと。 この行為をモデリングともいう。
そして、Rails では Active Record によってモデルを定義し、以下の役割を持つ。
1. DB と接続して、DB のレコードと Active Record オブジェクトを結びつける。
[主な機能の例]
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 というクラスメソッドを使うと、トークンによる検証をスキップできる。