パーフェクトRuby on Rails(増補改訂版)を読みました

特に覚えておきたいこと、読み直したいことについてメモを取りながら読みました。

1章 Ruby on Railsの概要

bin/rails db:console

設定ファイルに定義されている接続情報を使ってDBに接続し、コンソールを起動できる(便利)

さらに、DBコンソールからどのようなCREATE TABLE文が発行されたか確認する(これはRailsあんまり関係ない)

sqlite> .schema tasks CREATE TABLE IF NOT EXISTS "tasks" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "content" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);

http://localhost:portId/rails/info/routes にアクセスするとルーティング設定の確認ができる( bin/rails routes と同じ出力結果)

2章 Ruby on RailsMVC

ActiveRecord::Relation

ActiveRecordのQuery Interfaceによる操作結果をオブジェクトとして表現したもの。メソッド(メソッドチェインを含む)呼び出しを通じてActiveRecord::Relationは内部でどのようなSQLを発行するか、という情報だけを保持する。実際にそのSQLの実行結果が必要になるまでは、データベースに対するアクセスは発生しない。

明示的に任意の箇所でSQLを発行したい場合は、to_aメソッドを呼び出して即座にSQLを発行するというテクニックがある。

Scopeとクラスメソッドの違い

多くの場合、Scopeはクラスメソッドのショートハンドとして利用できる。

find_byなどの、条件がマッチしなかった場合にnilが返るようなメソッドを使う場合に違いが生じる。

クラスメソッドで条件がマッチしなかった場合はnilが返るが、Scopeで条件がマッチしなかった場合は、その検索条件を除外したクエリを発行し、必ずActiveRecord::Relationを返すという動作をする。

そのため、nilを返す必要がある場合は、クラスメソッドとして定義する方が良い。

バリデーション

アプリケーションレイヤでの制御を通じてエラーを取り扱いやすくするためのもの。

モデルのバリデーションで多くの検証を行うことができるが、レースコンディションですり抜けてしまう問題などもあり得るので、2度手間に感じてもnot null制約などDB上で利用できる制約は活用しておく。

フック

aroundフックは、アクションの前後で実行する都合上、呼び出すメソッドではbefore相当の処理を実行した後でyieldを使ってアクション側に処理を戻してあげる必要がある。

class BooksController < ApplicationController
  around_action :action_logger, only: [:destroy]

  def destroy
    # ...
  end

  private

  def action_logger
    logger.info "around-before"
    yield
    logger.info "around-after"
  end
end

RailsにおけるCSRF対策

Railsでは通常、GET以外のリクエストではセキュリティトークンが必須となっていて、formヘルパーなどを利用していると暗黙的にformで送信するパラメータとして含まれるようになっている

特殊なルーティング

memberを利用すると、「/publishers/:id/detail」のように個別のリソースに対してアクションを設定します。

collectionを利用すると、「/publishers/search」のように、全体のリソースに対するアクションを設定します。

Rails.application.routes.draw do
  resources :publishers do
    member do
      get 'detail'
    end

    collection do
      get 'serach'
    end
  end
end

URLヘルパーメソッドの生成するパスをコンソール上で確認する

appオブジェクトを経由することで試せる

irb(main):001:0> app.edit_profile_path
=> "/profile/edit"
irb(main):002:0> app.edit_profile_url
=> "http://www.example.com/profile/edit"

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

RackとRailsの関係

Rackとは、WebアプリケーションサーバとWebアプリケーションフレームワーク間のインターフェイスを共通化した仕様であり実装となっているライブラリです。Rackを仲介できるインターフェイスを持つことでUnicornやPumaといったアプリケーションサーバRailsをはじめとするフレームワーク間でスムーズなやり取りが行えるようになっています。

Rackアプリケーションとはcallメソッドの引数として受け取ったHTTPリクエストデータなどを元にレスポンスとして返す内容を決定し、callメソッドの戻り値とするという行為に他なりません。(callメソッドはRackに対応するアプリケーションのインターフェイスとして用意しておくことが規約で定められている)

複数DBへの接続

実運用では、負荷を分散するために書き込み専用のprimary dbと読み込み専用のreplica dbを使用したいケースが多い。

それらをRails6標準機能で楽に管理できる。

4章 フロントエンドの開発手法

Rails5.2までは、JSの管理はSprocketsをデフォルトで使用していたが、6.0からはWebpackerを使ったJS管理がデフォルトに変更された。

Webpackerとは、JS管理のデファクトスタンダードであるWebpackを、Rails開発者用に必要な設定をあらかじめ済ませたもの。

Rails6.0の時点では、CSSや画像は引き続きSprocketsを使用して管理している。

assets:precompileで生成した静的ファイルの読み取り

  • プリコンパイル
    • 開発時は画面にアクセスするタイミングで動的にビルドされているが、本番環境ではあらかじめビルド済みのCSSファイルを配信することでアクセスするユーザへの待ち時間を発生させないようにしている
    • assets:precompileを実行するとCSSだけではなくJavaScriptファイルのビルドも行われる
    • assets:precompileはdevelopment環境とproduction環境の両方で動作するが、development環境では画面アクセス時に動的にビルドする状態で扱うことが一般的なため、通常assets:precompileで生成した静的ファイルを扱うのはproduction環境でRailsサーバを起動した場合となる

rails-ujs

画面の制御を補助するJSライブラリ。submitボタン押下時にダイアログを表示したり、入力フォームの二重送信をしないようにする制御など。直接JSを書かず、ビューテンプレートに少し記述を追加するだけで実現できる。

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

ActionCableやActionMailboxなどの説明の章。かなり具体的で本番運用を想定した設定などが乗っていた。

ActionMailbox、ActionCable、ActionTextは使ったことなかったが、この章でそれぞれ少しずつ触れることができた。

6章 Railsアプリケーション開発

awesomeeventsのサンプルレポジトリ

https://github.com/perfect-ruby-on-rails/awesome_events

OAuth&Githubで認証

ログイン確認のような、複数のコントローラで使用する汎用的な機能は一般的にApplicationControllerに記述する

ログイン状態を毎リクエスト確認するメソッドもApplicationControllerにbefore_actionフィルタと一緒に書いておいて、デフォルトでログインしていなければいけない状態にすると漏れ(本当はログインしていないと見れないページが非ログイン状態で見れてしまう)が発生しにくい。(helperとかに書きがちだった気がする)

SJR(Server-generated JavaScript Responses)

サーバサイドでJavaScriptを生成してクライアントサイドはそれをそのまま実行する形式。サーバサイド側でHTMLを生成し、クライアント側では受け取ったHTMLをそのまま使うだけになるので、フロントエンド部分をほとんど書く必要がない。

DHHはAjaxリクエストのバリデーションエラーの実装はSJRで対応するのが良いと考えている。(form_withメソッドはデフォルトでAjaxでフォームの情報を送信する)

https://github.com/rails/rails/issues/25197

ReactやVueと真っ向から対立するので、作りたいアプリやメンバーのスキルに応じて導入するか選択する(フロントをそこまでリッチにしなくて良い&専任のフロントエンドがいないならSJRは選択肢の候補に上がりそう)

ActiveRecordのモデルを絞り込む時は関連が使えるならなるべく関連を辿った方が良い。外部キーからfind_byなどで絞り込むこともできるが、関連を辿った方が何が起こっているのか理解しやすく、事故も起こりにくい。

ヘルパーについて

MVCアーキテクチャの原則としてビューにビジネスロジックは書かない。が、ビューの整形に関するロジックを書きたくなる場合がある。

直接ビューにロジックを書くと見通しが悪くなるので、ヘルパーにメソッドを追加してその中にロジックを書くと良い。

しかし、ヘルパーはグローバルな関数のように振る舞うので、ヘルパーにメソッドが増えてくると、何に使われているメソッドなのか分かりにくなる&そもそもヘルパーメソッドの書き方はオブジェクト志向でない。

上記課題を解決するためにDraperやActiveDecoratorなどのプレゼンター用gemを利用して、モデルに紐づいた形でビュー用のロジックを書く方法を採用する方法がある。多くのロジックをビューで利用する場合に導入を検討すると良い。

コントローラの粒度と名前付け

例えば退会処理を行う場合、それはUser#destroyで表現することができる。実際これでも全く問題ない。

しかし、「退会の意思確認ページ」を表示しようと思った場合、それに対応するアクションは何になるのか?おそらく基本の7アクション(index, new, create, show, edit, update, destroy)以外の新しいアクションを新設することになる。

基本の7アクション以外のアクションをコントローラに追加していくと、プロジェクトが進むにつれ1つのコントローラにアクションが増えてしまい、コントローラの見通しが悪くなってくる。

これを避けるための1つの指針として、「基本の7アクション以外のアクションを作成したくなったら別のコントローラを作る」というものがある。

退会処理の例でいうと、User#destroyの代わりに、Retirementsコントローラを作成しRetirements#createで退会処理を表現することができる。

コントローラは関連するモデルと同じ名前にする、という固定観念から離れて別のリソースとしてコントローラを定義することを考えてみよう。

dependent: :nullify

レコードを削除した時に、関連するレコードの外部キーをnullに更新できる

7章 Railsアプリケーションのテスト

サンプルコードはminitest。普段書かないので勉強になった。

8章 Railsアプリケーションの拡張・運用

activestorageとかkaminariとかelasticsearchとかエラーハンドリングの話題。

elasticsearchの起動忘れるとサーバエラーで先に進めなくなるので注意

9章 コード品質をあげる

rubocop_todo.ymlについて

bundle exec rubocop --auto-gen-config によってエラーを抑制する設定が rubocop_todo.yml に書き込まれる。

その後の使い方としては、 rubocop_todo.yml を読んで、チームのコードスタイル方針に合っているから抑制したままで良い項目については、その設定を .rubocop.yml に移動し、コードを修正すべき項目についてはそのまま rubocop_todo.yml に置いておく。

10章 コンテナを利用したRailsアプリケーションの運用

buildkitを利用してイメージビルドを高速化する

マウントキャッシュを利用して、イメージビルド中に書き込んだファイルをホストマシン上にキャッシュして保存しておくことができる

docker-compose

複数のコンテナイメージを組み合わせて一度に起動してくれる(rails, mysql, redis ...etc)

コンテナに馴染む設定ファイルの記述

ファイルベースで各環境用(prod, dev, test)の設定ファイルを配置してから起動するという今までの方式は、完結したファイルシステムの状態でパッケージングされその状態のまま起動できることがメリットであるコンテナに馴染まない。

なので、起動時に可変する設定値を注入するようにする。そのために環境変数を用いる。

秘匿情報についてはcredentials.yml.encを利用する。暗号化されているのでそのままコンテナイメージに含めてしまい、master key を環境変数として実行時に渡すことでコンテナ起動時に秘匿値を復号化し読み込むことができる。

上記のように環境変数を使用することで、環境毎にコンテナイメージを作ってそれぞれのアクセス権限を管理する必要は無くなるが、一方で環境変数に指定する値を誰がどこで管理するのかという問題が生じる。

クラウドサービスを活用する

クラウドサービスの活用

上記のような権限管理をクラウドプラットフォームに任せる。

KMSで作成した鍵情報を元にファイルを暗号化して、S3もしくはGCSにファイルとして保存しておく方法がある。コンテナ起動時にラッパースクリプトを実行するようにしておいて、ファイルをストレージからダウンロードしつつ復号化し、それからアプリケーションを起動する。

RailsとHTTPサーバの関係

nginxなどのhttpサーバをRailsアプリケーションの前段におく必要性

  • railsアプリケーションサーバとして多く利用されているunicornの特性。unicornはマルチプロセスを基盤にしたアプリケーションサーバで、外部のインターネットから直接接続を受け取ると、接続先との不安定な通信状況や大きなレイテンシの影響でI/O待ちが発生し、プロセスが長時間占有されてしまう。そういったI/O待ちによる無駄を避けるために、内部ネットワークで高速に通信が可能で大量のコネクションをバッファしておける場所としてnginxなどのHTTPサーバが必要になる
  • SSLを直接Railsで受けるよりも、nginx等を終端にした方が扱いが簡単

HTTPサーバは基本的に必要だと考えておく。

11章 複雑なドメインを表現する

ドメイン

アプリケーションが対象とする問題領域のこと

モデリング

ドメインを分析して構成概念を抽出すること

ドメインモデル

モデリングした結果得られた概念のこと

ドメインモデルは、その概念に関連する属性と振る舞いを持ったオブジェクトとして定義されます。この振る舞いのことをドメインロジック、またはビジネスロジックと呼びます。Railsのモデルは、ドメインモデルとドメインロジックを実装するレイヤーになります。 ドメインモデルは単にドメインの概念を抽出しただけのものなので、ほとんどの場合、これだけではアプリケーションを形にすることはできません。何らかのデータストアを利用して、ドメインモデルの状態をデータとして保存する必要があります。Railsでは、データストアにRDBを用いることを想定しています。そして、アクティブレコードと呼ばれるアーキテクチャパターンを用いて、データベースのレコードとオブジェクトを対応づけています。そのため、モデルの実装で利用するライブラリの名前がActiveRecordなのです。

アクティブレコードとは

アクティブレコードは、データの取得・保存処理とドメインロジックを合わせてカプセル化するアーキテクチャパターンです。Rubyのようなクラスベースのオブジェクト志向言語で実装する場合、データベースのテーブルをクラス、レコードをクラスのインスタンス、カラムをインスタンスの属性に対応させます。アクティブレコードを実装したクラスでは、SQLの実行結果からクラスのインスタンスを構築し、インスタンスメソッドを介して特定のレコードを操作します。アクティブレコードはデータベースのテーブルとクラスが直接対応しているため、単純で理解しやすいという利点があります。一方で、その構造上、単体では複雑なドメインロジックを表現しきれないという欠点があります。

サービスオブジェクトの実装ルールやインタフェース

  • サービスオブジェクトを実装するクラスの名前は、ある1つのドメインロジックを指すものにする。そして、このロジックを実行するためのクラスメソッドを1つだけ公開するようにする。
  • クラスの外からはインスタンスを生成できないよう(intializeメソッドをprivateにする)にすることで、サービスオブジェクトに状態を持たせないようにする。これは、入力が同じであれば常に同じ結果を返すというサービスオブジェクトの性質を満たす上で重要

https://qiita.com/joker1007/items/25de535cd8bb2857a685

サービスクラス等という単語に踊らされる前に、オブジェクト指向プログラミングに関する知識を貯めて、真剣にクラス同士の関係性を設計しロジックを表現する努力をしようということだ。

12章 複雑なユースケースを実現する

アプリケーション開発におけるユースケースとは

何らかの目的を達成するために行われるユーザとアプリケーションの間の一連のやりとりを表したもの(GitHubアカウントでログインする、など)

Railsにおけるユースケース

RailsはURLで表されるリソースをデータベースのテーブルと1対1に対応させており(慣例的に、リソースと同名のモデルのCRUD操作を行うため)、これらのCRUD操作を通じてユーザとやり取りすると言える。1つまたは複数のCRUD操作によって、1つのユースケースが構成される。

ActiveModelを使ってデータベースと紐づかないモデルを作った

フォームオブジェクト

ユースケースのロジックを実装するオブジェクト(インタラクター)に、form_withとの連携に必要なインタフェースを持たせたもの。ユーザとのやり取りに用いるフォームを簡単に実装できる

ActiveModel::ValidatorやActiveModel::EachValidatorを使用して共通のバリデーションルールを作った。

プレゼンター

app/helpersはグローバルスコープであるため、全てのビューヘルパーでメソッド名が重複しないように注意しなければならない。

プレゼンターというレイヤーを導入することはこの問題を解決する手段の1つ。

Railsにおいては、あるモデルがもつ属性やロジックを利用して、表示に関するロジックを実装するオブジェクトのことを指す。Decoratorと呼ばれるデザインパターンを用いて実装されることが多いため、デコレーターとも呼ばれる。

ActiveDecorator

モジュールとして定義されたプレゼンターを、対応するモデルの各インスタンスに動的にextendする。

13章 複雑なデータ操作を実現する

concernとかコールバックオブジェクトとか。必要になったらまた読み返そう。