Rails をはじめよう

このガイドでは、Ruby on Rails(以下Rails)を初めて設定して実行するまでを解説します。

このガイドの内容:

  • Railsのインストール方法、新しいRailsアプリケーションの作成方法、アプリケーションからデータベースへの接続方法
  • Railsアプリケーションの一般的なレイアウト
  • MVC(モデル・ビュー・コントローラ)およびRESTful設計の基礎
  • Railsアプリケーションで使うパーツを手軽に生成する方法
  • 作成したアプリケーションをKamalでデプロイする方法

目次

  1. はじめに
  2. Railsとは何か
  3. Railsアプリを新規作成する
  4. Hello, Rails!
  5. データベースモデルを作成する
  6. Railsコンソール
  7. Active Recordモデルの基礎
  8. Railsのリクエストの流れ
  9. ルーティング
  10. コントローラとアクション
  11. 認証機能を追加する
  12. 製品をキャッシュに乗せる
  13. フィールドをAction Textでリッチテキストにする
  14. Active Storageでファイルアップロード機能を追加する
  15. 国際化(I18n)
  16. 在庫の通知機能を追加する
  17. CSSとJavaScriptを追加する
  18. Railsでテストを書く
  19. RuboCopでコードの形式を統一する
  20. セキュリティチェック
  21. GitHub ActionsでCIを実行する
  22. Kamalでproduction環境にデプロイする
  23. 今後のステップ
  24. 参考資料(日本語)

1 はじめに

Ruby on Railsへようこそ! この「Railsをはじめよう」ガイドでは、Railsを活用してWebアプリケーションを構築するときの中核となる概念について解説します。本ガイドを理解するために、Railsの経験は必要ありません。

Railsは、Rubyプログラミング言語用に構築されたWebフレームワークです。RailsはRuby独自のさまざまな機能を活用しているため、このチュートリアルで紹介する基本的な用語や語彙を理解できるように、事前にRubyの基礎を学習しておくことを強くオススメします。

訳注:Railsガイドでは開発経験者が早くキャッチアップできるよう、多くの用語説明を省略しています。読んでいて「難しい」と感じた場合はRailsチュートリアルからお読みください。

2 Railsとは何か

Railsとは、プログラミング言語「Ruby」で書かれたWebアプリケーションフレームワークです。Railsは、あらゆる開発者がWebアプリケーション開発で必要となる作業やリソースを事前に想定することで、Webアプリケーションをより手軽に開発できるように設計されています。

Railsは、他の多くのWebアプリケーションフレームワークと比較して、アプリケーションを開発する際のコード量がより少なくて済むにもかかわらず、より多くの機能を実現できます。ベテラン開発者の多くが「RailsのおかげでWebアプリケーション開発がとても楽しくなった」と述べています。

Railsは「最善の開発方法は1つである」という、ある意味大胆な判断に基いて設計されています。何かを行うための最善の方法を1つ仮定して、それに沿った開発を全面的に支援します。言い換えれば、Railsで仮定されていない別の開発手法は行いにくくなります。

この「Rails Way」、すなわち「Railsというレールに乗って開発する」手法を学んだ人は、開発の生産性が驚くほど向上することに気付くでしょう。逆に、レールに乗らずに従来の開発手法にこだわると、開発の楽しさが減ってしまうかもしれません。

Railsの哲学には、以下の2つの主要な基本理念があります。

  • 繰り返しを避けよ(Don't Repeat Yourself: DRY): DRYはソフトウェア開発上の原則であり、「システムを構成する知識のあらゆる部品は、常に単一であり、明確であり、信頼できる形で表現されていなければならない」というものです。同じコードを繰り返し書くことを徹底的に避けることで、コードが保守しやすくなり、容易に拡張できるようになり、バグも減らせます。
  • 設定より規約が優先(Convention Over Configuration): Railsでは、Webアプリケーションの機能を実現する最善の方法が明確に示されており、Webアプリケーションの各種設定についても従来の経験や慣習を元に、それらのデフォルト値を定めています。デフォルト値が決まっているおかげで、開発者の意見をすべて取り入れようとした自由過ぎるWebアプリケーションのように、開発者が大量の設定ファイルを設定せずに済みます。

3 Railsアプリを新規作成する

ここでは、Railsの組み込み機能のいくつかをデモンストレーションするシンプルなeコマースアプリをstoreというプロジェクト名で構築します。

ドル記号$で始まるコマンドは、ターミナルで実行する必要があります。

3.1 前提条件

このプロジェクトでは以下のものが必要です。

  • Ruby 3.2以降
  • Rails 8.0.0以降
  • コードエディタ

RubyやRailsをインストールする必要がある場合は、Ruby on Rails インストールガイドに記載されている手順に従ってください。

訳注:GitHubが提供するクラウド開発環境『Codespaces』には、公式のRuby on Railsテンプレートが用意されています。Use this templateボタンから、ワンクリックでRailsを動かせるクラウド開発環境が手に入ります。(参考: GitHub Codespacesを利用する - Rails Girls

正しいバージョンのRailsがインストールされていることを確認しておきましょう。現在のバージョンを表示するには、ターミナルを開いて以下のコマンドを実行すると、バージョン番号が出力されます。

$ rails --version
Rails 8.0.0

バージョン番号はRails 8.0.0以降になるはずです。

3.2 最初のRailsアプリケーションを作成する

Railsには、作業を楽にするためのさまざまなコマンドが付属しています。 利用可能なコマンドをすべて表示するには、rails --helpを実行します。

rails newコマンドは、新しいRailsアプリケーションの基盤を生成するので、まずこのコマンドを実行することから始めましょう。

storeアプリケーションを作成するには、ターミナルで以下のコマンドを実行します。

$ rails new store

rails newコマンドにフラグを追加すると、Railsが生成するアプリケーションをカスタマイズできます。利用可能なオプションをすべて表示するには、rails new --helpを実行します。

新しいアプリケーションを作成したら、そのディレクトリに移動します。

$ cd store

3.3 ディレクトリ構造

新しいRailsアプリケーションに含まれるファイルとディレクトリを少し見てみましょう。このフォルダをコードエディタで開くか、ターミナルでls -laを実行してファイルとディレクトリを確認できます。

ファイル/フォルダ 目的
app/ このディレクトリには、アプリケーションのコントローラ、モデル、ビュー、ヘルパー、メーラー、ジョブ、そしてアセットが置かれます。以後、本ガイドでは基本的にこのディレクトリを中心に説明を行います。
bin/ このディレクトリには、アプリケーションを起動するrailsスクリプトが置かれます。セットアップ・アップデート・デプロイに使うスクリプトファイルもここに置けます。
config/ このディレクトリには、アプリケーションの各種設定ファイル(ルーティング、データベースなど)が置かれます。詳しくはRails アプリケーションの設定項目を参照してください。
config.ru Rackベースのサーバーでアプリケーションの起動に使われるRack設定ファイルです。
db/ このディレクトリには、現在のデータベーススキーマと、データベースマイグレーションファイルが置かれます。
Dockerfile Dockerの設定ファイルです。
Gemfile
Gemfile.lock
これらのファイルは、Railsアプリケーションで必要となるgemの依存関係を記述します。この2つのファイルはBundler gemで使われます。
lib/ このディレクトリには、アプリケーションで使う拡張モジュールが置かれます。
log/ このディレクトリには、アプリケーションのログファイルが置かれます。
public/ 静的なファイルやコンパイル済みアセットはここに置きます。このディレクトリにあるファイルは、外部(インターネット)にそのまま公開されます。
Rakefile このファイルは、コマンドラインから実行できるタスクを探索して読み込みます。このタスク定義は、Rails全体のコンポーネントに対して定義されます。独自のRakeタスクを定義したい場合は、Rakefileに直接書くと権限が強すぎるので、なるべくlib/tasksフォルダの下にRake用のファイルを追加してください。
README.md アプリケーションの概要を簡潔に説明するマニュアルをここに記入します。このファイルにはアプリケーションの設定方法などを記入し、これさえ読めば誰でもアプリケーションを構築できるようにしておきましょう。
script/ 使い捨ての、または汎用のスクリプトベンチマークをここに置きます。
storage/ このディレクトリには、ディスクサービス用のSQLiteデータベースファイルやActive Storageファイルが置かれます。詳しくはActive Storageの概要を参照してください。
test/ このディレクトリには、単体テストやフィクスチャなどのテスト関連ファイルを置きます。テストについて詳しくはRailsアプリケーションをテストするを参照してください。
tmp/ このディレクトリには、キャッシュやpidなどの一時ファイルが置かれます。
vendor/ サードパーティ製コードはすべてこのディレクトリに置きます。通常のRailsアプリケーションの場合、外部のgemファイルがここに置かれます。
.dockerignore コンテナにコピーすべきでないファイルをDockerに指示するのに使うファイルです。
.gitattributes このファイルは、gitリポジトリ内の特定のパスについてメタデータを定義します。このメタデータは、gitや他のツールで振る舞いを拡張できます。詳しくはgitattributesドキュメントを参照してください。
.github/ GitHub固有のファイルが置かれます。
.gitignore Gitに登録しないファイル(またはパターン)をこのファイルで指定します。Gitにファイルを登録しない方法について詳しくはGitHub - Ignoring filesを参照してください。
.kamal/ Kamalの秘密鍵とデプロイ用フックが含まれます。
.rubocop.yml このファイルにはRuboCop用の設定が含まれます。
.ruby-version このファイルには、デフォルトのRubyバージョンが記述されています。

3.4 MVCの基礎

Railsのコードは、MVC(Model-View-Controller)アーキテクチャに基づいて編成されています。MVCでは、コードの大部分が以下の3つの主要な概念に基づいて配置されます。

  • モデル: アプリケーション内のデータ(通常はデータベースのテーブル)を管理します。
  • ビュー: レスポンスをHTML、JSON、XMLなどのさまざまな形式でレンダリングします。
  • コントローラ: ユーザー操作や各リクエストのロジックを処理します。

MVCアーキテクチャの図

MVCの基本部分を理解したので、MVCがどのようにRailsで使われるかを見てみましょう。

4 Hello, Rails!

それでは、Railsサーバーを初めて起動してみましょう。

ターミナルでstoreディレクトリに移動し、以下のコマンドを実行します。

$ bin/rails server

すると、PumaというWebサーバーが起動します。Pumaサーバーは、静的ファイルやRailsアプリケーションの配信を担当します。

=> Booting Puma
=> Rails 8.0.0 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
*  Min threads: 3
*  Max threads: 3
*  Environment: development
*          PID: 12345
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

Railsアプリケーションを表示してみましょう。 ブラウザでhttp://localhost:3000を開くと、デフォルトのRailsウェルカムページが表示されます。

Rails起動ページのスクリーンショット

動きました!

Railsの起動ページは、新しいRailsアプリケーションの「スモークテスト」として使えます。このページが表示されれば、サーバーが正常に動作していることが確認できます。

実行されているターミナルのウィンドウでCtrl + Cキーを押せば、いつでもWebサーバーを停止できます。

4.1 開発中の自動コード読み込み

開発者の幸福はRailsの基本的な哲学であり、開発中にコードを自動で再読み込みする機能は、これを実現する方法の1つです。

Railsサーバーを起動すると、新しいファイルや既存のファイルへの変更が検出され、実行中も必要に応じてコードの読み込みや再読み込みが自動的に行われます。これにより、コード変更のたびにRailsサーバーを再起動しなくても済むので、アプリの構築に集中できます。

また、Railsアプリケーションでは、他のプログラミング言語で見られるようなrequireステートメントがほとんど使われていないことにも気付くでしょう。 Railsでは命名規則に基づいてファイルを自動的にrequireするので、アプリケーションコードの記述に集中できます。

詳しくは定数の自動読み込みと再読み込みガイドを参照してください。

5 データベースモデルを作成する

RailsのActive Recordは、リレーショナルデータベースをRubyコードにマッピングする機能であり、テーブルやレコードの作成、更新、削除など、データベースを操作するための構造化クエリ言語(SQL)を生成するのに役立ちます。

このstoreアプリケーションでは、RailsのデフォルトであるSQLiteをリレーショナルデータベースとして使っています。

それでは、このRailsアプリケーションにデータベーステーブルを追加して、シンプルな eコマースストアに製品を追加できるようにしてみましょう。

$ bin/rails generate model Product name:string

このコマンドは、string型のnameカラムを持つProductという名前のモデルをデータベースで生成するようにRailsに指示します。他のカラム型を追加する方法についてはこの後で学習します。

コマンドを実行すると、ターミナルに次の内容が表示されます。

      invoke  active_record
      create    db/migrate/20240426151900_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml

このコマンドは以下を行います。

  1. db/migrateフォルダの下にマイグレーションファイルを作成。
  2. app/models/product.rbというActive Recordモデルを作成。
  3. このモデルで使うテストファイルとフィクスチャ(fixture)ファイルを作成。

Railsのモデル名には英語の単数形を使います。これは、インスタンス化されたモデルはデータベース内の1件のレコードを表す(データベースに1個の製品(a product)を追加する)という考えに基づいています。

5.1 データベースのマイグレーション

マイグレーション(migration)とは、データベースに対して行う一連の変更のことです。

マイグレーションを定義することで、データベースのテーブルやカラム、およびその他の属性を追加・変更・削除するためにデータベースを変更する方法を統一された形でRailsに指示します。 これにより、自分のコンピュータ上での開発中に行ったデータベース変更をトラッキングして、production環境に安全にデプロイできるようにします。

Railsが作成したマイグレーションをコードエディタで開いて、マイグレーションで何が行われるかを確認してみましょう。マイグレーションファイルはdb/migrate/<タイムスタンプ>_create_products.rbに配置されます。

class CreateProducts < ActiveRecord::Migration[8.0]
  def change
    create_table :products do |t|
      t.string :name

      t.timestamps
    end
  end
end

このマイグレーションは、Railsにproductsという新しいデータベーステーブルを作成するよう指示しています。

モデル名はProductのように単数形を使いますが、データベーステーブル名はproductsのように複数形を使っている点にご注目ください。これは、データベースは各モデルの「すべての」インスタンスを保持する(つまり、製品の集まり(products)のデータベースを作成している)という考え方に基づいています。

次のcreate_tableブロックは、このデータベーステーブルで定義するカラムと型を定義します。

  • t.string :name: productsテーブルにnameというカラムを作成し、型をstringに設定するようRailsに指示します。

  • t.timestamps: モデルにcreated_at:datetimeupdated_at:datetimeの2つのカラムを一度に定義するショートカットです。 これらのカラムは、RailsのほとんどのActive Recordモデルで表示され、レコードの作成時や更新時にActive Recordによって自動的に値が設定されます。

5.2 マイグレーションを実行する

データベースに対して行う変更を定義したら、以下のコマンドを使ってマイグレーションを実行します。

$ bin/rails db:migrate

このbin/rails db:migrateコマンドは、新しいマイグレーションをチェックしてデータベースに適用します。

実行結果は以下のような感じになります。

== 20240426151900 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0030s
== 20240426151900 CreateProducts: migrated (0.0031s) ==========================

実行したマイグレーションに誤りがあった場合は、bin/rails db:rollbackを実行することで直前のマイグレーションに戻せます。

6 Railsコンソール

productsテーブルが作成されたので、Railsで操作できるようになりました。 さっそく試してみましょう。

ここでは、Railsコンソールと呼ばれる機能を使います。Railsコンソールは、Railsアプリケーションでコードを試すときに便利な対話型ツールです。

$ bin/rails console

上のRailsコンソールコマンドを実行すると、以下のようなプロンプトが表示されます。

Loading development environment (Rails 8.0.0)
store(dev)>

ここで入力した内容は、Enterを押すと実行されます。 それではRailsバージョンを出力してみましょう。

store(dev)> Rails.version
=> "8.0.0"

たしかに動きました!

7 Active Recordモデルの基礎

Railsのモデルジェネレーターを実行してProductモデルを作成すると、app/models/product.rbにファイルが作成されました。作成したファイルにある以下のクラスは、Active Recordを使ってデータベースのproductsテーブルとやり取りします。

class Product < ApplicationRecord
end

このProductクラスにコードがないことに驚くかもしれません。Railsはこのモデルの定義をどうやって知るのでしょうか?

このProductモデルが使われると、Railsはデータベーステーブルでカラム名と型を照会し、これらの属性のコードを自動的に生成します。Railsは、定型コードを記述する手間を省いて、代わりにバックグラウンドで処理してくれるので、開発者はアプリケーションロジックに集中できます。

このProductモデルでどんなカラムを検出されるかを、Railsコンソールで確認しましょう。

Railsコンソールで以下のコマンドを実行します。

store(dev)> Product.column_names

すると、以下のように表示されるはずです。

=> ["id", "name", "created_at", "updated_at"]

Railsは上記のカラム情報をデータベースに要求し、その情報を用いてProductクラスの属性を動的に定義するので、開発者が個別の属性を手動で定義する必要はありません。これは、Railsを使うことで開発がいかに簡単になるかを示す一例です。

7.1 レコードを作成する

Railsコンソールで以下のコードを実行すると、Productモデルの新しいレコードを作成できます。

store(dev)> product = Product.new(name: "T-Shirt")
=> #<Product:0x000000012e616c30 id: nil, name: "T-Shirt", created_at: nil, updated_at: nil>

このproduct変数は、Productモデルのインスタンスです。この時点ではまだデータベースに保存されていないため、idcreated_atupdated_atのタイムスタンプはありません。

レコードをデータベースに保存するには、saveを呼び出します。

store(dev)> product.save
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Create (0.9ms)  INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('T-Shirt', '2024-11-09 16:35:01.117836', '2024-11-09 16:35:01.117836') RETURNING "id" /*application='Store'*/
  TRANSACTION (0.9ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

saveが呼び出されると、Railsはメモリ上にある属性を取得し、SQLのINSERTクエリを生成してこのレコードをデータベースに挿入します。

このとき、データベースレコードのidと、created_atタイムスタンプおよびupdated_atタイムスタンプを用いて、メモリ上のオブジェクトも更新します。product変数の内容を表示してみれば、このことが確認できます。

store(dev)> product
=> #<Product:0x00000001221f6260 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">

createを使えば、1回の呼び出しでActive Recordオブジェクトのインスタンス化と保存を同時に実行できます。

store(dev)> Product.create(name: "Pants")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Create (0.4ms)  INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('Pants', '2024-11-09 16:36:01.856751', '2024-11-09 16:36:01.856751') RETURNING "id" /*application='Store'*/
  TRANSACTION (0.1ms)  COMMIT TRANSACTION /*application='Store'*/
=> #<Product:0x0000000120485c80 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">

7.2 レコードをクエリで取り出す

Active Recordモデルを使って、データベース内のレコードを検索することも可能です。

データベース内にあるProductの全レコードを検索するには、allメソッドを使います。 これはクラスメソッドなので、以下のようにProductクラスで直接呼び出せます(上記のsaveなど、Productのインスタンスで呼び出すインスタンスメソッドとは異なります)。

store(dev)> Product.all
  Product Load (0.1ms)  SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000121845158 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">,
 #<Product:0x0000000121845018 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]

これによりSELECT SQLクエリが生成され、productsテーブルからすべてのレコードを読み込まれます。各レコードは自動的にActive RecordのProductモデルのインスタンスに変換されるため、Rubyから手軽に操作できます。

allメソッドが返すActiveRecord::Relationオブジェクトは、配列に似たデータベースレコードのコレクションで、フィルタリングや並べ替えなどのデータベース操作を実行する機能を備えています。

7.3 レコードのフィルタリングと並べ替え

データベースから受け取った結果をフィルタで絞り込みたい場合は、以下のようにwhereメソッドでカラムごとにレコードをフィルタリングできます。

store(dev)> Product.where(name: "Pants")
  Product Load (1.5ms)  SELECT "products".* FROM "products" WHERE "products"."name" = 'Pants' /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x000000012184d858 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]

これにより、生成されたSELECT SQLクエリにWHERE句も追加され、"Pants"にマッチするnameを持つレコードがフィルタで絞り込まれます。同じ名前を持つレコードが複数返される可能性があるため、ここでもActiveRecord::Relationが返されます。

order(name: :asc)メソッドを使うと、レコードを以下のように名前のアルファベット昇順で並べ替えられます。

store(dev)> Product.order(name: :asc)
  Product Load (0.3ms)  SELECT "products".* FROM "products" /* loading for pp */ ORDER BY "products"."name" ASC LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000120e02a88 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">,
 #<Product:0x0000000120e02948 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">]

7.4 レコードを検索する

特定のレコードを1件検索したい場合はどうすればよいでしょうか?

これを行うには、findクラスメソッドでIDを指定する形で、1件のレコードを検索します。以下のコードは、findメソッドにID1を指定して呼び出しています。

store(dev)> Product.find(1)
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
=> #<Product:0x000000012054af08 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">

これにより、SELECTクエリが生成されますが、渡されたID 1にマッチするidカラムをWHEREで指定しています。また、返すレコードを1件のみに絞るため、LIMIT 1も追加されています。

ここでは、データベースからレコードを1件だけ取得したいので、ActiveRecord::RelationではなくProductモデルのインスタンスを取得します。

7.5 レコードを更新する

レコードを更新するには、「updateを使う」「属性を割り当ててからsaveを呼び出す」という2つの方法が使えます。

Productモデルのインスタンスでupdateを呼び出し、新しい属性のハッシュを渡してデータベースに保存できます。これにより、「属性の割り当て」「バリデーションの実行」「変更のデータベースへの保存」を1回のメソッド呼び出しでまとめて実行できます。

store(dev)> product = Product.find(1)
store(dev)> product.update(name: "Shoes")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Update (0.3ms)  UPDATE "products" SET "name" = 'Shoes', "updated_at" = '2024-11-09 22:38:19.638912' WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (0.4ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

これにより、データベース内の製品名が"T-Shirt"から"Shoes"に更新されます。 Product.allを再度実行してこれを確認してみましょう。

store(dev)> Product.all

製品名はShoesとPantsの2つになっていることがわかります。

  Product Load (0.3ms)  SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012c0f7300
  id: 1,
  name: "Shoes",
  created_at: "2024-12-02 20:29:56.303546000 +0000",
  updated_at: "2024-12-02 20:30:14.127456000 +0000">,
 #<Product:0x000000012c0f71c0
  id: 2,
  name: "Pants",
  created_at: "2024-12-02 20:30:02.997261000 +0000",
  updated_at: "2024-12-02 20:30:02.997261000 +0000">]

2番目の方法として、属性を割り当て、変更をバリデーションしてデータベースに保存する準備を終えてから、saveを呼び出す方法も使えます。

今度は、"Shoes"という製品名を"T-Shirt"に戻してみましょう。

store(dev)> product = Product.find(1)
store(dev)> product.name = "T-Shirt"
=> "T-Shirt"
store(dev)> product.save
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Update (0.2ms)  UPDATE "products" SET "name" = 'T-Shirt', "updated_at" = '2024-11-09 22:39:09.693548' WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (0.0ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

7.6 レコードを削除する

データベースからレコードを削除するには、destroyメソッドを使います。

store(dev)> product.destroy
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Destroy (0.4ms)  DELETE FROM "products" WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (0.1ms)  COMMIT TRANSACTION /*application='Store'*/
=> #<Product:0x0000000125813d48 id: 1, name: "T-Shirt", created_at: "2024-11-09 22:39:38.498730000 +0000", updated_at: "2024-11-09 22:39:38.498730000 +0000">

これにより、データベースから"T-Shirt"製品が削除されました。Product.allでこれを確認すると、パンツのみが返されることが分かります。

store(dev)> Product.all
  Product Load (1.9ms)  SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012abde4c8
  id: 2,
  name: "Pants",
  created_at: "2024-11-09 22:33:19.638912000 +0000",
  updated_at: "2024-11-09 22:33:19.638912000 +0000">]

7.7 バリデーション

Active Recordは、データベースに挿入したデータが特定のルールに準拠していることを保証するためのバリデーション(validation: 検証)機能を提供しています。

すべての製品にnameカラムが存在することを保証するために、Productモデルにpresenceバリデーションを追加してみましょう。

# app/models/product.rb
class Product < ApplicationRecord
  validates :name, presence: true
end

Railsは開発中にコードの変更を自動的に再読み込みすると冒頭で説明したことを思い出すかもしれません。 ただし、コードを更新したときにコンソールが実行中の場合は、コンソールから手動で更新する必要があります。それでは、reload!を実行して更新を反映してみましょう。

store(dev)> reload!
Reloading...

今度は、Railsコンソールでわざとnameを指定せずにProductインスタンスを作成してみましょう。

store(dev)> product = Product.new
store(dev)> product.save
=> false

今回は、name属性が指定されていないため、savefalseを返します。

Railsは、作成・更新・保存の操作中にバリデーションを自動的に実行して、有効な入力であることを保証します。

バリデーションによって生成されたエラーのリストを表示するには、以下のようにインスタンスでerrorsを呼び出します。

store(dev)> product.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=name, type=blank, options={}>]>

これは、存在チェックのエラーを詳しく知らせてくれるActiveModel::Errorsオブジェクトを返します。

また、ユーザーインターフェイスに表示できるわかりやすいエラーメッセージを生成することも可能です。

store(dev)> product.errors.full_messages
=> ["Name can't be blank"]

次は、この製品をブラウザで表示するためのWebインターフェースを構築しましょう。

Railsコンソールはひとまずおしまいにします。exitを実行してコンソールを終了できます。

8 Railsのリクエストの流れ

Railsで「Hello」を表示するには、、少なくとも「ルーティング」と「コントローラ」、そしてコントローラに付随する「アクション」と「ビュー」を作成する必要があります。

  • ルーティング(route): 受け取ったリクエストを、適切なコントローラのアクションに対応付けます。
  • コントローラアクション(controller action): リクエストを処理し、ビューに表示するデータを準備します。
  • ビュー(view): データを表示するためのテンプレートです。

これらは、実装の観点では以下のようになります。

ルーティングはRubyのDSL(ドメイン固有言語)で記述されたルールです。 コントローラは普通のRubyクラスであり、そのpublicメソッドがアクションになります。 ビューはテンプレートであり、通常はHTMLとRubyを組み合わせて記述されます。

以上はごく簡単な説明ですが、次にこれらの各ステップについてさらに詳しく説明します

9 ルーティング

Railsのルーティング(route、routing)はURLを構成する要素の1つであり、受信したHTTPリクエストを適切なコントローラとアクションに転送することでリクエストの処理方法を決定します。

まず、URLとHTTPリクエストメソッドについて簡単に復習しましょう。

9.1 URLの構成要素

URLがどのような要素から構成されているかを詳しく見てみましょう。

http://example.org/products?sale=true&sort=asc

上のURLの各要素には、以下のような名前があります。

  • httpsの部分はプロトコル(protocol)と呼ばれます
  • example.orgの部分はホスト(host)と呼ばれます
  • /productsの部分はパス(path)と呼ばれます
  • ?sale=true&sort=ascの部分はクエリパラメータ(query parameters)と呼ばれます

9.2 HTTPメソッドとその目的

HTTPリクエストは、特定のURLに対してサーバーが実行すべきアクションを指示するときにHTTPメソッド(HTTP verb: HTTP動詞とも呼ばれます)を利用します。

最も一般的なHTTPメソッドは次のとおりです。

  • GETリクエスト: 特定のURLのデータを取得するようサーバーに指示します(ページの読み込みやレコードの取得など)。
  • POSTリクエスト: 処理を実行するためのデータをURLに送信します(通常は新しいレコードを作成します)。
  • PUTまたはPATCHリクエスト: 既存のレコードを更新するためのデータをURLに送信します。
  • DELETEリクエスト: URLに送信されると、レコードを削除するようサーバーに指示します。

9.3 Railsのルーティング

Railsにおけるルーティングは、HTTPメソッドとURLパスをペアにしたコード行を指します。 ルーティングは、どのcontrolleractionでリクエストに応答すべきかをRailsに指示します。

Railsでルーティングを定義するには、コードエディタを再び開いて、config/routes.rbファイル内のルーティングに、以下のgetで始まる行を追加します。

Rails.application.routes.draw do
  # (省略)
  get "/products", to: "products#index"  # この行を追加
end

このルーティングは、/productsパスへのGETリクエストを探索するようRailsに指示します。この例では、リクエストのルーティング先として"products#index"を指定しています。

マッチするリクエストが見つかると、RailsはそのリクエストをProductsControllerというコントローラ内のindexアクションに送信します。Railsはこのようにしてリクエストを処理し、ブラウザにレスポンスを返します。

上のルーティングでは、「プロトコル」「ドメイン」「クエリパラメータ」の指定が不要であることに気付くでしょう。その理由は、リクエストは基本的にプロトコルとドメインによってサーバーに確実に届くためです。Railsはリクエストを取得すると、定義済みのルーティング基づいて、リクエストに応答するためのパスを認識します。

なお、クエリパラメータは、Railsがリクエストに適用できるオプションのようなもので必須ではなく、通常はコントローラでデータをフィルタリングするときに使われます。

Railsのルーティングの流れ

別の例も見てみましょう。 前述のルーティングの下に、以下の行を追加します。

post "/products", to: "products#create"

ここでは、/productsパスへのPOSTリクエストを受け取ったら、ProductsControllercreateアクションでリクエストを処理するようRailsに指示しています。

ルーティングでは、特定のパターンを持つURLにマッチさせる必要が生じる場合もあります。 では以下のルーティングは、どのように機能するかおわかりでしょうか?

get "/products/:id", to: "products#show"

このルーティングのパスには:idが含まれています。これはパラメータ(parameter、paramsとも)と呼ばれ、後でリクエストを処理するときに使うURLの一部がここにキャプチャされます。

たとえばユーザーが/products/1というパスにアクセスすると、:idパラメータが1に設定され、コントローラアクションでIDが1の製品レコードを検索して表示できるようになります。 /products/2は同様に、IDが2の製品を表示するのに使えます。

ルーティングのパラメータには、整数以外のものも使えます。

たとえば、さまざまな記事を含むブログサービスでは、以下のルーティングで/blog/hello-worldとマッチするようになります。

get "/blog/:title", to: "blog#show"

以下のルーティングでは、/blog/hello-worldからhello-worldというパラメータをslugとしてキャプチャし、このパラメータにマッチするタイトルのブログ投稿を検索できるようになります。

get "/blog/:slug", to: "blog#show"
9.3.1 CRUDのルーティング

リソースへの操作で通常必要となる一般的な操作は、「作成」「読み取り」「更新」「削除」の4つであり、CRUDと呼ばれます。

これは、7つの一般的なコントローラアクションに相当します。

  • index: すべてのレコードを表示します
  • new: 新しいレコード1件を作成するためのフォームをレンダリングします
  • create: newのフォーム送信を処理し、エラーを処理してレコードを1件作成します
  • show: 指定のレコード1件をレンダリングして表示します
  • edit: 指定のレコード1件を更新するためのフォームをレンダリングします
  • update: editのフォーム送信を処理し、エラーを処理してレコードを1件更新します
  • destroy: 指定のレコード1件を削除します

これらのCRUDアクションのルーティングは、以下のように書くことで追加することも一応可能です。

get "/products", to: "products#index"

get "/products/new", to: "products#new"
post "/products", to: "products#create"

get "/products/:id", to: "products#show"

get "/products/:id/edit", to: "products#edit"
patch "/products/:id", to: "products#update"
put "/products/:id", to: "products#update"

delete "/products/:id", to: "products#destroy"
9.3.2 リソースルーティング

これら8つのルーティングを毎回入力するのは冗長なので、Railsではルーティングを1行で定義できるショートカットを提供しています。

上記のルーティングを以下の1行に置き換えて、上と同じCRUDアクションをすべて作成できるようにしましょう。

resources :products

CRUDアクションの一部しか使わない場合は、必要なアクションだけを正確に指定し、使わないアクションは無効にしておきましょう。詳しくはルーティングガイドを参照してください。

9.4 ルーティングコマンド

Railsには、アプリケーションが応答するルーティングをすべて表示するコマンドが用意されています。

ターミナルを開いて以下のコマンドを実行します。

$ bin/rails routes

resources :productsで生成されたルーティングが以下のように表示されます。

                                  Prefix Verb   URI Pattern                                                                                       Controller#Action
                                products GET    /products(.:format)                                                                               products#index
                                         POST   /products(.:format)                                                                               products#create
                             new_product GET    /products/new(.:format)                                                                           products#new
                            edit_product GET    /products/:id/edit(.:format)                                                                      products#edit
                                 product GET    /products/:id(.:format)                                                                           products#show
                                         PATCH  /products/:id(.:format)                                                                           products#update
                                         PUT    /products/:id(.:format)                                                                           products#update
                                         DELETE /products/:id(.:format)                                                                           products#destroy

表示されるルーティングには、上の他にもヘルスチェックなどの他の組み込みのRails機能によるルーティングが含まれているのがわかります。

訳注: 開発中のRailsサーバーでは、http://localhost:3000/rails/info/routesにブラウザでアクセスすることでルーティング情報を表示できます。

10 コントローラとアクション

製品のルーティングを定義したので、次はコントローラとアクションを実装して、これらのURLへのリクエストを処理できるようにしましょう。

以下のbin/rails generateコマンドは、indexアクションを含むProductsControllerを生成します。ルーティングは既に設定したので、--skip-routesフラグでジェネレータでのルーティング生成部分をスキップできます。

$ bin/rails generate controller Products index --skip-routes
      create  app/controllers/products_controller.rb
      invoke  erb
      create    app/views/products
      create    app/views/products/index.html.erb
      invoke  test_unit
      create    test/controllers/products_controller_test.rb
      invoke  helper
      create    app/helpers/products_helper.rb
      invoke    test_unit

このコマンドを実行すると、コントローラ用に以下のさまざまなファイルが生成されます。

  • コントローラファイル自身(products_controller.rb
  • 生成したコントローラで利用するビューを保存するフォルダ(app/views/products/
  • コントローラ生成時に指定したアクションに対応するビューファイル(index.html.erb
  • このコントローラ用のテストファイル(products_controller_test.rb
  • ビューのロジックを切り出して配置するためのヘルパーファイル(products_helper.rb

app/controllers/products_controller.rbで定義されているProductsControllerをエディタで開くと、以下のような感じになっているはずです。

class ProductsController < ApplicationController
  def index
  end
end

products_controller.rbというファイル名は、このファイルで定義されているProductsControllerというクラス名を小文字に変えてアンダースコア区切りに変更したものであることに気付くかもしれません。この命名パターンを守ることで、他の言語で見られるようなrequireを使わなくても、Railsがコードを自動的に読み込めるようになります。

ここでのindexメソッドはアクションです。メソッドの中身は空ですが、メソッドが空の場合は、デフォルトでアクション名と一致する名前のテンプレートをレンダリングするようになっているので問題ありません。

indexアクションを実行すると、app/views/products/index.html.erbをレンダリングします。このファイルをコードエディタで開くと、レンダリングされるHTMLが以下のように表示されます。

<h1>Products#index</h1>
<p>Find me in app/views/products/index.html.erb</p>

10.1 リクエストを作成する

作成した結果をブラウザで確認してみましょう。

まず、ターミナルでbin/rails serverを実行してRailsサーバーを起動します。 次に、ブラウザでhttp://localhost:3000を開くと、Railsのウェルカムページが表示されます。

ブラウザでhttp://localhost:3000/productsを開くと、Railsは製品のindexページのHTMLをレンダリングします。

このときの処理の流れは以下のようになります。

  1. ブラウザが/productsパスへのリクエストを送信すると、ルーティングはproducts#indexにマッチします。
  2. 次にRailsは、このリクエストをProductsControllerに送信してindexアクションを呼び出します。
  3. indexアクションは空なので、Railsはこのコントローラアクションに一致するapp/views/products/index.html.erbテンプレートをレンダリングして、ブラウザにレスポンスを返します。

なお、config/routes.rbファイルに以下の行を追加すると、rootパスにアクセスしたときのルーティングでProductsindexアクションをレンダリングするようにRailsに指示できます。

root "products#index"

これで、http://localhost:3000にアクセスすると、RailsがProducts#indexをレンダリングするようになります。

10.2 インスタンス変数

さらに先に進んで、データベースにあるレコードをいくつかレンダリングしてみましょう。

indexアクションを更新して以下のようにデータベースクエリを追加し、それをインスタンス変数に割り当ててみましょう。Railsのコントローラでは、ビューにデータを渡すときにインスタンス変数(@で始まる変数)が使われます。

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end
end

次に、app/views/products/index.html.erbのビューテンプレートファイル内にあるHTMLを以下のERBコードに置き換えます。

<%= debug @products %>

ERBはEmbedded Rubyの略で、Rubyコードを実行してRailsでHTMLを動的に生成できるようにします。

<%= %>タグは、その内側に書いたRubyコードを実行して戻り値をブラウザで出力するようERBに指示します。この場合、@productsを受け取ったものがdebugでYAMLに変換され、YAMLが出力されます。

ブラウザでhttp://localhost:3000/を更新すると、出力結果が変更されたことがわかります。表示されているのは、データベース内のレコードがYAML形式に変換されたものです。

debugヘルパーは、デバッグで役立つように変数をYAML形式で出力します。 たとえば、コントローラで複数形の@productsのつもりでうっかり単数形の@productを書いてしまった場合、変数がコントローラで正しく設定されていないことを突き止めるのに可能性があります。

その他に利用可能なヘルパーについて詳しくはAction Viewヘルパーガイドを参照してください。

次は、ビューにすべての製品名がリスト表示されるようにしてみましょう。 app/views/products/index.html.erbを以下のように更新します。

<h1>Products</h1>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <%= product.name %>
    </div>
  <% end %>
</div>

ERB内のコードは、ActiveRecord::Relationオブジェクトである@products内の各製品をループし、製品名を含む<div>タグをレンダリングします。

ここでも新しいERBタグが使われています。 <% %>の中に書いたRubyコードは実行時に評価されますが、戻り値をブラウザに出力しない点が<%= %>と異なります。これにより、そのままだとHTMLに不要な配列を出力する@products.eachの出力が無視されるようになります。

10.3 CRUDアクション

次は、個別の製品を1件ずつ表示できるようにする必要があります。これは、リソースを読み取るためのCRUDのR(Read)に相当します。

製品へのルーティングは、既にresources :productsルーティングでまとめて定義してあるので、products#showを指すルーティングとして/products/:id が生成されるようになっています。

次に、これに対応するshowアクションをProductsControllerに追加して、呼び出されたときの振る舞いを定義する必要があります。

10.4 個別の製品を表示する

ProductsControllerをエディタで開いて、以下のようにshowアクションを追加します。

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end
end

indexアクションのときは、複数の製品を読み込むために複数形の@productsを使いましたが 、このshowアクションは、データベースから1件のレコードを読み込む(つまり1件の製品(one product)を表示する)ので、単数形の@productを定義します。

データベースにクエリをかけるのに使うリクエストパラメータには、paramsでアクセスします。 この場合、/products/:idルーティングの:idが使われます。 ユーザーがブラウザで/products/1にアクセスすると、paramsハッシュに{id: 1}が含まれるので、showアクションでProduct.find(1)を呼び出すと、IDが1の製品がデータベースから読み込まれます。

次に、showアクションに対応したビューが必要です。ProductsControllerはRailsの命名規則に沿って、app/views/フォルダの下のproducts/という名前のサブフォルダにビューファイルが置かれていることを想定しています。

showアクションが必要としているapp/views/products/show.html.erbファイルをエディタで作成して、以下の内容を追加します。

<h1><%= @product.name %></h1>

<%= link_to "Back", products_path %>

indexページに個別のshowページへのリンクを追加して、クリックして個別の製品ページに移動できるようにしておくと便利です。

そこで、app/views/products/index.html.erbビューを以下のように更新して、新しく作ったshowページへのリンクを追加しましょう。showアクションへのパスには<a>タグを利用できます。

<h1>Products</h1>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <a href="/products/<%= product.id %>">
        <%= product.name %>
      </a>
    </div>
  <% end %>
</div>

ブラウザでindexページを再読み込みすると、期待通りにリンクが表示されたことがわかります。 しかしこれはさらに改善できます。

Railsは、パスとURLを生成するためのヘルパーメソッドを提供します。 bin/railsroutesを実行すると以下のように表示されるPrefix列のproductsproductは、RubyコードでURLを生成できるヘルパーメソッド名に対応します。

                                  Prefix Verb   URI Pattern                                                                                       Controller#Action
                                products GET    /products(.:format)                                                                               products#index
                                 product GET    /products/:id(.:format)                                                                           products#show

これらのルーティングプレフィックスに対応するヘルパーメソッドは、以下のようになります。

  • products_path: "/products"`というパスを生成する
  • products_url: "http://localhost:3000/products"`というURLを生成する
  • product_path(1): "/products/1"というパスを生成する
  • product_url(1): "http://localhost:3000/products/1"というURLを生成する

プレフィックス名_pathは、ブラウザが現在のドメインであると理解する相対パスを返します。

プレフィックス名_urlは、「プロトコル」「ホスト」「ポート番号」を含む完全なURLを返します。

URLヘルパーは、ブラウザの外部で表示されるメールをレンダリングする場合に便利です。

URLヘルパーをlink_toヘルパーによる<a>タグ生成と組み合わせると、タグを直接書かずにRubyだけできれいにリンクを生成できるようになります。link_toには、リンクの表示名(ここではproduct.name)と、href属性で使うリンク先のパスまたはURL(ここではproduct)を渡せます。

これらのヘルパーを使ってapp/views/products/index.html.erbをリファクタリングすると、以下のように簡潔なビューコードになります。

<h1>Products</h1>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <%= link_to product.name, product %>
    </div>
  <% end %>
</div>

10.5 製品を作成する

これまでは製品をRailsコンソールで作成するしかありませんでしたが、今度はブラウザで製品を作成できるようにしましょう。

製品を作成するには、以下の2つのアクションを作成する必要があります。

  1. newアクション: 製品情報を収集するためのフォームを作成する
  2. createアクション: 製品を保存してエラーをチェックする

まずはコントローラでnewアクションを作成しましょう。

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end
end

newアクションは、フォームフィールドの表示で使う新しいProductをインスタンス化します。

次はapp/views/products/index.html.erbを以下のように更新して、newアクションにリンクできるようにします。

<h1>Products</h1>

<%= link_to "New product", new_product_path %>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <%= link_to product.name, product %>
    </div>
  <% end %>
</div>

newアクションに対応するapp/views/products/new.html.erbビューテンプレートを以下の内容で作成して、新しいProductのフォームをレンダリングできるようにします。

<h1>New product</h1>

<%= form_with model: @product do |form| %>
  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

このnew.html.erbビューでは、Railsのform_withヘルパーを製品作成用のHTMLフォーム生成に利用しています。このform_withヘルパーは、フォームビルダーを用いてCSRFトークン生成などの処理を行い、model:で指定されたものに基づいてURLを生成するとともに、送信ボタンのテキストにモデル名を反映することまで行います。

このページをブラウザで開いてソースを表示すると、フォームのHTMLは以下のようになります。

<form action="/products" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="UHQSKXCaFqy_aoK760zpSMUPy6TMnsLNgbPMABwN1zpW-Jx6k-2mISiF0ulZOINmfxPdg5xMyZqdxSW1UK-H-Q" autocomplete="off">

  <div>
    <label for="product_name">Name</label>
    <input type="text" name="product[name]" id="product_name">
  </div>

  <div>
    <input type="submit" name="commit" value="Create Product" data-disable-with="Create Product">
  </div>
</form>

Railsのフォームビルダーによって、セキュリティ用のCSRFトークンやUTF-8サポート用のaccept-charset="UTF-8"属性がフォームに組み込まれ、個別の入力フィールド名が設定され、Create Product送信ボタンも無効な状態で追加されています。

フォームビルダーに新しいProductインスタンスを渡したので、新規レコード作成用のデフォルトルーティングである/productsパスにPOSTリクエストを送信するように構成されたフォームが、自動的に生成されました。

ここから送信されるフォームを処理するには、まずコントローラにcreateアクションを以下のように実装する必要があります。

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def product_params
      params.expect(product: [ :name ])
    end
end
10.5.1 Strong Parameters

createアクションはフォームから送信されたデータを処理しますが、セキュリティのためにパラメータをフィルタリングしておかなければなりません。ここで役に立つのが、privateメソッドとして追加したproduct_paramsメソッドです。

訳注: メソッド名のproductの部分はモデル名と同じ単数形にするのが慣例です)。

product_paramsメソッドは、リクエストで受け取ったパラメータを検査して、パラメータの配列を値として持つ:productというキーが必ず存在することを保証します。ここでは、製品に許可されているパラメータは:nameのみなので、これ以外のどんなパラメータをRailsに渡しても無視されます(エラーにはなりません)。これにより、アプリケーションをハッキングしようとする悪意のあるユーザーからアプリケーションが保護されます。

詳しくはStrong Parameterを参照してください。

10.5.2 エラー処理

product_paramsを使ってこれらのパラメータを新しいProductに割り当てたら、データベースへの保存を試みる準備が整います。@product.saveは、バリデーションを実行してレコードをデータベースに保存するようActive Recordに指示します。

saveが成功すると、新しい製品のshowページにリダイレクトします。redirect_toに Active Recordオブジェクトを渡すと、そのレコードのshowアクションへのパスが生成されます。

redirect_to @product

上を実行すると、@productProductモデルのインスタンスなので、リダイレクト用に"/products/2"パスを生成します。このとき、パス内ではモデル名Productを複数形のproductsにしたうえで、オブジェクトID 2を末尾に追加します。

saveが失敗して、レコードが有効にならなかった場合、同じフォームを再レンダリングして、ユーザーが無効なデータを修正できるようにします。createアクションのelseではrender :newをレンダリングするように指示しています。

RailsはProductsコントローラにいることを認識しているので、app/views/products/new.html.erbビューテンプレートをレンダリングする必要があります。

createアクションでは@product変数が設定済みなので、@productをデータベースに保存できなかった場合でも、このビューテンプレートを再度レンダリングするとフォームにProductデータが自動的に入力されます。

また、HTTPステータスを422 Unprocessable Entityに設定して、ブラウザにこのPOSTリクエストが失敗したことを伝えて、それに応じた処理に備えます。

10.6 製品を編集する

レコードを編集する処理は、レコードを作成する処理と非常に似ています。レコード作成ではnewアクションとcreateアクションを使いましたが、レコード編集では代わりにeditアクションとupdateアクションを使います。

コントローラで以下のコードを実装してみましょう。

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @product = Product.find(params[:id])
  end

  def update
    @product = Product.find(params[:id])
    if @product.update(product_params)
      redirect_to @product
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def product_params
      params.expect(product: [ :name ])
    end
end

次に、app/views/products/show.html.erbビューテンプレートにEditページへのリンクを追加します。

<h1><%= @product.name %></h1>

<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>
10.6.1 before_actionコールバックでコードをDRYにする

editアクションとupdateアクションは、showと同様に既存のデータベースレコードが存在している必要があります。before_actionを使うことで、そのための同じコードの重複を排除できます。

before_actionを使うと、アクション間で共有されているコードを抽出して、アクションの直前に実行できます。

上のコントローラでは、@product = Product.find(params[:id])という同じコードがshoweditupdateという3つの異なるメソッドで定義されています。このクエリをset_productというbeforeアクションに抽出しておけば、3つのアクションで同じコードを書かずに済むのでコードが簡潔になります。

これは、DRY(Don't Repeat Yourself: 繰り返しを避けよ)原則が実際に機能している良い例です。

class ProductsController < ApplicationController
  before_action :set_product, only: %i[ show edit update ]

  def index
    @products = Product.all
  end

  def show
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
  end

  def update
    if @product.update(product_params)
      redirect_to @product
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def set_product
      @product = Product.find(params[:id])
    end

    def product_params
      params.expect(product: [ :name ])
    end
end
10.6.2 ビューをパーシャルに切り出す

新しい製品を作成するためのフォームは既に作成しましたが、このフォームを編集や更新のフォームでも再利用できたら便利だと思いませんか?これは、複数の場所でビューを再利用できるようにするパーシャル(partial)という機能を使ってフォームをapp/views/products/_form.html.erbというパーシャルファイルに切り出すことで実現できます。

パーシャルのファイル名は、これがパーシャルであることを示すためにアンダースコア_で始まります。

それと同時に、ビューで使われているインスタンス変数をすべてローカル変数に置き換えたいと思います。ローカル変数は、パーシャルをレンダリングするときに定義できます。これを行うには、パーシャル内の@productを以下のようにproductに置き換えます。

<%= form_with model: product do |form| %>
  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

ローカル変数を使うと、値だけが異なるパーシャルを同じページで繰り返し再利用できるようになります。これは、indexページのように多数のアイテムのリストをレンダリングするときに便利です。

作成したこのパーシャルをapp/views/products/new.html.erbビューで使うには、フォームの部分を以下のようにパーシャルのrender呼び出しに置き換えます。

<h1>New product</h1>

<%= render "form", product: @product %>
<%= link_to "Cancel", products_path %>

Editビューも、フォームの_form.html.erbパーシャルのおかげで、Newビューとほぼ同じように書けます。

以下の内容でapp/views/products/edit.html.erbを作成しましょう。

<h1>Edit product</h1>

<%= render "form", product: @product %>
<%= link_to "Cancel", @product %>

ビューのパーシャルについて詳しくは、Action Viewガイドを参照してください。

10.7 製品を削除する

実装が必要な最後の機能は、製品の削除です。 DELETE /products/:idリクエストを処理するために、ProductsControllerdestroyアクションを追加しましょう。

before_action :set_productコールバックにdestroyを追加すると、他のアクションと同じ方法で@productインスタンス変数を設定できるようになります。

class ProductsController < ApplicationController
  before_action :set_product, only: %i[ show edit update destroy ]

  def index
    @products = Product.all
  end

  def show
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
  end

  def update
    if @product.update(product_params)
      redirect_to @product
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @product.destroy
    redirect_to products_path
  end

  private
    def set_product
      @product = Product.find(params[:id])
    end

    def product_params
      params.expect(product: [ :name ])
    end
end

製品を削除できるようにするには、app/views/products/show.html.erbで以下のようにDeleteボタンを追加する必要があります。

<h1><%= @product.name %></h1>

<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>

button_toは、Deleteというテキストが表示されたボタンを1つ持つフォームを生成します。このボタンをクリックすると、フォームが/products/:idDELETEリクエストとして送信され、コントローラのdestroyアクションがトリガーされます。

なお、turbo_confirmデータ属性は、フォームを送信する前にユーザーに確認ダイアログを表示するようにTurboというJavaScriptライブラリに指示します。これについては、後ほど詳しく説明します。

11 認証機能を追加する

今のままでは誰でも製品を編集・削除できてしまうので、安全ではありません。製品管理でユーザー認証を必須にすることで、セキュリティを強化しましょう。

ここでは、Railsに付属している認証機能ジェネレータが利用できます。この認証機能ジェネレータを使って、UserモデルとSessionモデル、アプリケーションにログインするために必要なコントローラとビューを作成できます。

再びターミナルを開いて、以下の認証機能ジェネレータコマンドを実行します。

$ bin/rails generate authentication

続いて以下のマイグレーションを実行し、Userモデル用のusersテーブルと、Sessionモデル用のsessionsテーブルをデータベースに追加します。

$ bin/rails db:migrate

ユーザーを作成するために、Railsコンソールを開きます。

$ bin/rails console

Railsコンソールでユーザーを作成するには、User.create!メソッドを実行します。以下の例の通りでなくても構わないので、独自のメールアドレスやパスワードを自由に使えます。

store(dev)> User.create! email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t"

Railsコンソールを終了し、ジェネレータが追加したbcrypt gemを反映するために以下のコマンドを実行してRailsサーバーを再起動します(BCryptは認証用のパスワードを安全にハッシュ化するのに使われます)。

$ bin/rails server

ブラウザでRailsアプリを開くと、どのページにアクセスしてもユーザー名とパスワードの入力を求められるようになります。

http://localhost:3000/products/newをブラウザで開いて、Userレコードの作成時に入力したメールアドレスとパスワードを入力してみましょう。

正しいユーザー名とパスワードを入力するとページにアクセスできるようになります。また、ブラウザは今後のリクエストに備えてこうしたcredential(認証情報)を保存するので、ページビューを移動するたびに入力する必要はありません。

11.1 ログアウト機能を追加する

アプリケーションからログアウトするためのボタンをapp/views/layouts/application.html.erbレイアウトファイルの冒頭に追加しましょう。このレイアウトには、ヘッダーやフッターなど、すべてのページで使うHTMLを配置します。

以下のように、<body>タグ内にホームへのリンクとログアウトボタンを含む小さな<nav>セクションを追加し、Rubyのyieldメソッドを<main>タグで囲みます。

<!DOCTYPE html>
<html>
  <!-- (省略) -->
  <body>
    <nav>
      <%= link_to "Home", root_path %>
      <%= button_to "Log out", session_path, method: :delete if authenticated? %>
    </nav>

    <main>
      <%= yield %>
    </main>
  </body>
</html>

ユーザーが認証済みの場合にのみ、Log outボタンが表示されます。 Log outボタンをクリックすると、session_pathDELETEリクエストが送信されて、ユーザーがログアウトします。

11.2 認証なしのアクセスも許可する

ただし、ストアの製品indexページとshowページは誰でもアクセスできるようにしておく必要があります。Railsの認証ジェネレータは、デフォルトではすべてのページへのアクセスを認証済みユーザーのみに制限します。

ゲストが製品を表示できるようにするには、コントローラで以下のように認証なしのアクセスを許可します。

class ProductsController < ApplicationController
  allow_unauthenticated_access only: %i[ index show ]
  # (省略)
end

ログアウトしてから再び製品のindexページとshowページにアクセスし、認証なしでアクセスできるかどうかを確認してみてください。

11.3 認証済みユーザーにだけリンクを表示する

製品を作成してよいのはログイン済みのユーザーだけにしておきたいので、app/views/products/index.html.erbビューのlink_to行を以下のように変更して、ユーザーが認証済みの場合にのみNew productリンクを表示するようにしましょう。

<%= link_to "New product", new_product_path if authenticated? %>

Log outボタンをクリックすると、indexページのNew productリンクが非表示になります。http://localhost:3000/session/newをブラウザで開いてログインすると、indexページにNew productリンクが表示されます。

オプションとして、先ほどのapp/views/layouts/application.html.erbレイアウトの<nav>セクションに以下のルーティングへのリンクも追加して、認証されていない場合はLoginリンクを表示するようにしてもよいでしょう。

<%= link_to "Login", new_session_path unless authenticated? %>

また、app/views/products/show.html.erbビューのEditリンクとDestroyリンクを以下のように更新して、認証済みの場合にのみEditリンクとDestroyリンクを表示するようにしてもよいでしょう。

<h1><%= @product.name %></h1>

<%= link_to "Back", products_path %>
<% if authenticated? %>
  <%= link_to "Edit", edit_product_path(@product) %>
  <%= button_to "Destroy", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
<% end %>

12 製品をキャッシュに乗せる

ページの特定の部分をキャッシュすると、パフォーマンスが向上する場合があります。 Railsは、データベース上に構築されるキャッシュストアであるSolid Cacheをデフォルトで組み込むことで、このプロセスを簡素化しています。

cacheメソッドを使うと、HTMLをキャッシュに保存できます。app/views/products/show.html.erb<h1>見出しを以下のように囲んで、キャッシュを有効にしてみましょう。

<% cache @product do %>
  <h1><%= @product.name %></h1>
<% end %>

@productcacheメソッドに渡すと、製品に固有のキャッシュキーを生成します。Active Recordオブジェクトにあるcache_keyメソッドは、"products/1"のような文字列を返します。ビューのcacheヘルパーは、これをテンプレートダイジェストと組み合わせて、このHTMLに固有のキーを作成します。

developmentモードでキャッシュを有効にするには、ターミナルで次のコマンドを実行します。

$ bin/rails dev:cache

製品のshowアクション(/products/2など)にアクセスすると、Railsのサーバーログに新しいキャッシュ行が表示されます。

Read fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (1.6ms)
Write fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (4.0ms)

キャッシュを有効にしてからこのページを初めて開くと、Railsはキャッシュキーを生成して、キャッシュストアが存在するかどうかを問い合わせます。これがログのRead fragment行です。

これは初めて表示したページビューなのでキャッシュは存在せず、HTMLが生成されてキャッシュに書き込まれます。これはログのWrite fragment行として確認できます。

ページを更新すると、ログにWrite fragmentが出力されなくなることがわかります。

Read fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (1.3ms)

キャッシュエントリは最後のリクエストによって書き込まれたため、Railsは2回目のリクエストでキャッシュエントリを見つけます。また、Railsはレコードが更新されるとキャッシュキーを変更して、古いキャッシュデータが誤ってレンダリングされないようにします。

詳しくは、Rails のキャッシュガイドを参照してください。

13 フィールドをAction Textでリッチテキストにする

リッチテキスト機能やマルチメディア要素の埋め込み機能は、多くのアプリケーションで求められています。RailsのAction Textを使えば、こうした機能をすぐに使えるようになります。

Action Textの利用を開始するには、まずインストーラーを実行します。

$ bin/rails action_text:install
$ bundle install
$ bin/rails db:migrate

すべての新機能が読み込まれていることを確認するために、Railsサーバーを再起動します。

次に、リッチテキストのdescription(説明)フィールドを製品に追加してみましょう。

まず、Productモデルに以下のコードを追加します。

class Product < ApplicationRecord
  has_rich_text :description
  validates :name, presence: true
end

これで、app/views/products/_form.html.erbのフォームを以下のように更新して、送信ボタンの上に説明の編集用リッチテキストフィールドを追加できるようになります。

<%= form_with model: product do |form| %>
  <%# (省略) %>

  <div>
    <%= form.label :description, style: "display: block" %>
    <%= form.rich_text_area :description %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

この新しいパラメータをフォームで送信するには、app/controllers/products_controller.rbコントローラ側で許可する必要があるので、expectで許可するパラメータを以下のように更新して、descriptionを追加します。

    # 信頼できるパラメータリストのみを許可する
    def product_params
      params.expect(product: [ :name, :description ])
    end

さらに、showビュー(app/views/products/show.html.erb)のcacheブロックも以下のように更新して、説明フィールドを表示する必要があります。

<% cache @product do %>
  <h1><%= @product.name %></h1>
  <%= @product.description %>
<% end %>

ビューが変更されると、Railsによって生成されるキャッシュキーも変更されるので、キャッシュがビューテンプレートの最新バージョンと同期した状態が維持されます。

それでは、新しい製品を作成して、descriptionフィールドに太字や斜体のテキストを追加してみましょう。製品を作成すると、書式付きテキストがShowページに表示されるようになり、製品を編集すると、このリッチテキストがテキスト領域に保持されるようになります。

詳しくは、Action Textの概要を参照してください。

14 Active Storageでファイルアップロード機能を追加する

Action Textは、ファイルのアップロードを手軽に行えるActive StorageというRailsの別の機能の上に構築されています。

製品のEditページを開いて、適当な画像をリッチテキストエディタにドラッグしてから、レコードを更新してみてください。画像がアップロードされて、リッチテキストエディタ内で表示されていることがわかります。素晴らしいですね!

Active Storageは、Action Textと別に直接利用することも可能です。

Productモデルに製品画像を添付する機能も追加してみましょう。

class Product < ApplicationRecord
  has_one_attached :featured_image
  has_rich_text :description
  validates :name, presence: true
end

続いて、app/views/products/_form.html.erbフォームのSubmitボタンの上に、以下のようにファイルアップロード用フィールドを追加します。

<%= form_with model: product do |form| %>
  <%# (省略) %>

  <div>
    <%= form.label :featured_image, style: "display: block" %>
    <%= form.file_field :featured_image, accept: "image/*" %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

これまでと同様に、:featured_imageも許可済みパラメータとしてapp/controllers/products_controller.rbに追加します。

    # 信頼できるパラメータリストのみを許可する
    def product_params
      params.expect(product: [ :name, :description, :featured_image ])
    end

最後に、app/views/products/show.html.erbの冒頭に以下のコードを追加して、製品画像を表示できるようにしましょう。

<%= image_tag @product.featured_image if @product.featured_image.attached? %>

http://localhost:3000/products/newをブラウザで開いて、Featured imageの「ファイルを選択」ボタンをクリックして製品画像をアップロードしてみると、保存後にshowページに画像が表示されるようになります。

詳しくは、Active Storage の概要を参照してください。

15 国際化(I18n)

Railsを使えば、アプリを他の言語に翻訳しやすくなります。

ビューのtranslateヘルパー(短縮形はt)は、名前で訳文を検索して、現在のロケール設定に合うテキストを返します。

app/products/index.html.erb<h1>見出しタグを以下のように更新して、見出しに訳文が使われるようにしてみましょう。

<h1><%= t "hello" %></h1>

ページを更新すると、見出しテキストがProductsからHello worldに変わっていることがわかります。このテキストはどこから来たのでしょうか?

デフォルトの言語は英語(en)なので、Railsはconfig/locales/en.yml(これはrails newで自動作成されます)を探索し、このenロケールの中にある以下のキー(訳文サンプル)にマッチします。

en:
  hello: "Hello world"

それでは、日本語用の新しいロケールファイルを作成してみましょう。エディタでconfig/locales/ja.ymlファイルを作成し、以下の訳文を追加します。

ja:
  hello: "こんにちは、世界"

次に、どのロケールを利用するかをRailsに伝える必要があります。

最も手軽な方法は、ロケールパラメータをURLから探すことです。app/controllers/application_controller.rbで以下のコードを追加することで、これを実現できます。

class ApplicationController < ActionController::Base
  # (省略)

  around_action :switch_locale

  def switch_locale(&action)
    locale = params[:locale] || I18n.default_locale
    I18n.with_locale(locale, &action)
  end
end

このコードはすべてのリクエストで実行され、パラメータ内のlocaleを探索して、見つからない場合はデフォルトのロケール(通常はen)にフォールバックします。これに基づいてリクエストのロケールを設定し、完了後にリセットします。

  • http://localhost:3000/products?locale=enをブラウザで開くと、英文が表示されます。
  • http://localhost:3000/products?locale=jaをブラウザで開くと、日本語の訳文が表示されます。
  • ロケールを指定しないhttp://localhost:3000/productsをブラウザで開くと、英語にフォールバックします。

次は、indexページの<h1>見出しのサンプル訳文を、実際の訳文に差し替えてみましょう。app/views/products/index.html.erbの見出しを以下のように更新します

<h1><%= t ".title" %></h1>

titleの直前の.は、ロケールを相対検索することを表す指示です。相対検索では、キーにコントローラ名とアクション名が自動的に含まれるため、コントローラ名やアクション名を毎回入力せずに済みます。英語ロケールで.titleを指定すると、実際にはen.products.index.titleが検索されます。

config/locales/en.ymlでは、以下のようにproductsindexを追加し、その下にtitleキーを追加することで、コントローラ名.ビュー名.訳文名と一致するようにします。

en:
  hello: "Hello world"
  products:
    index:
      title: "Products"

日本語のロケールファイルにも、以下のように英語ロケールファイルと対応する形で訳文を追加します。

ja:
  hello: "こんにちは、世界"
  products:
    index:
      title: "製品"

これで、http://localhost:3000/?locale=enで英語ロケールを表示すると「Products」が表示され、http://localhost:3000/?locale=jaで日本語ロケールを表示すると「製品」が表示されるようになります。

詳しくはRails 国際化(I18n)APIガイドを参照してください。

16 在庫の通知機能を追加する

製品の在庫が復活したときに通知を受け取るための電子メールを登録する機能は、eコマースストアでよく使われる機能です。Railsの基本についてひととおり確認したので、今度はこの機能をストアに追加してみましょう。

16.1 基本的な在庫トラッキング機能

まず、Productモデルにinventory_count(在庫数)を追加して、在庫数をトラッキングできるようにしましょう。以下のコマンドを実行してマイグレーションを生成します。

$ bin/rails generate migration AddInventoryCountToProducts inventory_count:integer

続いて以下のコマンドでマイグレーションを実行します。

$ bin/rails db:migrate

製品のapp/views/products/_form.html.erbフォームにも、以下のように在庫数のフィールドを追加する必要があります。

<%= form_with model: product do |form| %>
  <%# (省略) %>

  <div>
    <%= form.label :inventory_count, style: "display: block" %>
    <%= form.number_field :inventory_count %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

app/controllers/products_controller.rbコントローラでも、expectで許可するパラメータに:inventory_countを追加する必要があります。

    def product_params
      params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
    end

在庫数が決して負の数にならないようにしておくと便利なので、app/models/product.rbモデルにそのためのバリデーションも追加します。

class Product < ApplicationRecord
  has_one_attached :featured_image
  has_rich_text :description

  validates :name, presence: true
  validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end

以上の変更により、ストア内の製品の在庫数を更新できるようになりました。

16.2 通知の購読者を製品に追加する

商品の在庫が復活したことをユーザーに通知するには、在庫情報の購読者(subscriber)をトラッキングする機能が必要です。

購読希望者のメールアドレスを保存して個別の商品に関連付けるためのSubscriberというモデルを生成しましょう。

$ bin/rails generate model Subscriber product:belongs_to email

続いて新しいマイグレーションを実行します。

$ bin/rails db:migrate

上のコマンドでproduct:belongs_toオプションを指定したことで、購読者と製品が1対多リレーションを持つことを表すbelongs_to :productという宣言がSubscriberモデルに含まれるようになります。つまり、Subscriberモデルのインスタンスは1つのProductインスタンスに「属する(belongs to)」ということです。

ただし、1つの製品に購読者が複数存在する可能性もあるため、Productモデルにもhas_many :subscribers, dependent: :destroyを手動で追加することで、2つのモデル同士の関連付けの残りの部分も指定します。これにより、2つのデータベーステーブル間のクエリをjoin(結合)する方法が Railsで認識されます。

class Product < ApplicationRecord
  has_many :subscribers, dependent: :destroy
  has_one_attached :featured_image
  has_rich_text :description

  validates :name, presence: true
  validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end

次は、購読者を作成するためのコントローラが必要です。app/controllers/subscribers_controller.rbコントローラを以下の内容で作成しましょう。

class SubscribersController < ApplicationController
  allow_unauthenticated_access
  before_action :set_product

  def create
    @product.subscribers.where(subscriber_params).first_or_create
    redirect_to @product, notice: "You are now subscribed."
  end

  private

  def set_product
    @product = Product.find(params[:product_id])
  end

  def subscriber_params
    params.expect(subscriber: [ :email ])
  end
end

上のcreateアクションでは、作成後リダイレクトしたときにflashで通知メッセージを設定しています。Railsのflashは、リダイレクト後のページに表示するメッセージを保存するのに使われます。

このflashメッセージを表示するには、app/views/layouts/application.html.erbレイアウトの<body>タグで以下のように通知を追加します。

<html>
  <!-- (省略) -->
  <body>
    <div class="notice"><%= notice %></div>
    <!-- (省略) -->
  </body>
</html>

ユーザーが特定の製品を指定して通知を購読できるようにするため、SubscriberがどのProductに属しているかをネステッドルーティングで指定しましょう。

config/routes.rbファイルのresources :productsを以下のように変更します。

  resources :products do
    resources :subscribers, only: [ :create ]
  end

製品のShowページに在庫数を表示して、在庫があるかどうかをチェックできるようにしましょう。在庫がない場合は、在庫切れのメッセージと購読用のフォームを表示して、在庫が復活したときにユーザーが通知をメールで受け取れるようにします。

在庫表示用のapp/views/products/_inventory.html.erbパーシャルを以下の内容で作成します。

<% if product.inventory_count? %>
  <p><%= product.inventory_count %> in stock</p>
<% else %>
  <p>Out of stock</p>
  <p>Email me when available.</p>

  <%= form_with model: [product, Subscriber.new] do |form| %>
    <%= form.email_field :email, placeholder: "you@example.com", required: true %>
    <%= form.submit "Submit" %>
  <% end %>
<% end %>

次に、app/views/products/show.html.erbcacheブロックの下に以下のコードを追加して、上のパーシャルをレンダリングします。

<%= render "inventory", product: @product %>

16.3 「在庫あり」メールによる通知機能を追加する

商品の在庫が復活したときに購読者に通知する機能には、Railsのメール送信機能であるAction Mailerを使うことにします。

以下のコマンドを実行することでメーラーを生成できます。

$ bin/rails g mailer Product in_stock

これにより、app/mailers/product_mailer.rbin_stockメソッドを持つクラスが生成されます。

購読者のメールアドレスにメールを送信できるようにするには、このin_stockメソッドを以下のように更新します。

class ProductMailer < ApplicationMailer
  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.product_mailer.in_stock.subject
  #
  def in_stock
    @product = params[:product]
    mail to: params[:subscriber].email
  end
end

Action Mailerのジェネレータを実行すると、app/views/フォルダの下にもメールテンプレートが2つ生成されます。1つはHTMLメールの送信用、もう1つはテキストメールの送信用です。 これらのテンプレートを更新することで、「在庫あり」メッセージや製品へのリンクをメールに含められるようになります。

app/views/product_mailer/in_stock.html.erbを以下のように変更します。

<h1>Good news!</h1>

<p><%= link_to @product.name, product_url(@product) %> is back in stock.</p>

app/views/product_mailer/in_stock.text.erbを以下のように変更します。

Good news!

<%= @product.name %> is back in stock.
<%= product_url(@product) %>

メールクライアントでユーザーがリンクをクリックしたときにブラウザで開くようにするには、相対パスではなく完全なURLが必要なので、メーラーではproduct_pathではなくproduct_urlを使います。

bin/rails consoleでRailsコンソールを開いて、以下のように送信先の製品とサブスクライバを読み込むことで、メールをテストできます。

store(dev)> product = Product.first
store(dev)> subscriber = product.subscribers.find_or_create_by(email: "subscriber@example.org")
store(dev)> ProductMailer.with(product: product, subscriber: subscriber).in_stock.deliver_later

メールが送信されたことがRailsのlog/development.logで以下のように確認できます。

ProductMailer#in_stock: processed outbound mail in 63.0ms
Delivered mail 66a3a9afd5d4a_108b04a4c41443@local.mail (33.1ms)
Date: Fri, 26 Jul 2024 08:50:39 -0500
From: from@example.com
To: subscriber@example.com
Message-ID: <66a3a9afd5d4a_108b04a4c41443@local.mail>
Subject: In stock
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_66a3a9afd235e_108b04a4c4136f";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


----==_mimepart_66a3a9afd235e_108b04a4c4136f
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

Good news!

T-Shirt is back in stock.
http://localhost:3000/products/1


----==_mimepart_66a3a9afd235e_108b04a4c4136f
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<!-- BEGIN app/views/layouts/mailer.html.erb --><!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <!-- BEGIN app/views/product_mailer/in_stock.html.erb --><h1>Good news!</h1>

<p><a href="http://localhost:3000/products/1">T-Shirt</a> is back in stock.</p>
<!-- END app/views/product_mailer/in_stock.html.erb -->
  </body>
</html>
<!-- END app/views/layouts/mailer.html.erb -->
----==_mimepart_66a3a9afd235e_108b04a4c4136f--

Performed ActionMailer::MailDeliveryJob (Job ID: 5e2bd5f2-f54f-4088-ace3-3f6eb15aaf46) from Async(default) in 111.34ms

これらのメールをトリガーするには、在庫数が0から正の数に変わるたびにメールを送信するコールバックをProductモデルに追加します。

class Product < ApplicationRecord
  has_one_attached :featured_image
  has_rich_text :description
  has_many :subscribers, dependent: :destroy

  validates :name, presence: true
  validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }

  after_update_commit :notify_subscribers, if: :back_in_stock?

  def back_in_stock?
    inventory_count_previously_was.zero? && inventory_count > 0
  end

  def notify_subscribers
    subscribers.each do |subscriber|
      ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
    end
  end
end

上で追加したafter_update_commitは、変更がデータベースに保存された直後に実行されるActive Recordコールバックです。 if: :back_in_stock?は、back_in_stock?メソッドがtrueを返す場合にのみコールバックを実行するように指示します。

Active Recordで属性の変更をトラッキングできるようにするために、back_in_stock?メソッドではinventory_countの直前の値を確認するのにinventory_count_previously_wasinventory_count属性から自動生成された属性名_previously_wasメソッド)を使っています。続いて、その値を現在の在庫数と比較して、製品の在庫が復活したかどうかを判断します。

notify_subscribersは、特定の製品のすべての購読者のリストを得るためにActive Record関連付けを利用してsubscribersテーブルを照会してから、個別の購読者に送信するin_stockメールをキューに登録します。

16.4 共通コードをconcernに抽出する

このProductモデルには、通知を処理するためのコードがかなり多く含まれています。Railsでは、これをActiveSupport::Concernに切り出すことでコードを整理できます。concernは通常のRubyモジュールですが、使いやすくするためのシンタックスシュガーが追加されています。

最初に、Notificationsモジュールを作成してみましょう。

app/models/product/ディレクトリを作成してから、app/models/product/notifications.rbファイルを以下の内容で作成します。

module Product::Notifications
  extend ActiveSupport::Concern

  included do
    has_many :subscribers, dependent: :destroy
    after_update_commit :notify_subscribers, if: :back_in_stock?
  end

  def back_in_stock?
    inventory_count_previously_was == 0 && inventory_count > 0
  end

  def notify_subscribers
    subscribers.each do |subscriber|
      ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
    end
  end
end

このモジュールがクラスにincludeされると、includedブロック内に記述したコードは、最初からそのクラスの一部であるかのように実行されます。また、このモジュール内で定義したメソッドは、そのクラスのオブジェクト(インスタンス)で呼び出せる通常のインスタンスメソッドになります。

通知をトリガーするコードをNotificationモジュールに切り出したので、app/models/product.rbモデルで以下のようにNotificationsモジュールをincludeしてコードを簡潔にできます。

class Product < ApplicationRecord
  include Notifications

  has_many :subscribers, dependent: :destroy
  has_one_attached :featured_image
  has_rich_text :description

  validates :name, presence: true
  validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }

  # (省略)
end

concernは、Railsアプリケーションの機能を整理するための優れた手法のひとつです。製品に機能を繰り返し追加していると、やがてクラスが乱雑になります。代わりに、concernを使って各機能をProduct::Notificationsなどの自己完結型モジュールに抽出できます。このモジュールには、サブスクライバの処理と通知の送信方法に関する機能がすべて含まれています。

コードをconcernに抽出すると、機能の再利用性も高まります。たとえば、サブスクライバ通知も必要とする新しいモデルを手軽に導入できるようになります。このモジュールを利用する複数のモデルで同じ機能を提供できます。

16.5 通知購読の解除リンクをメールに追加する

通知を購読したユーザーが購読を解除できるようにしておきたいので、次は購読の解除機能を構築することにしましょう。

最初に、config/routes.rbのルーティングに購読解除用のルーティングを追加する必要があります。これは解除用メールに含めるURLとして使われます。

  resource :unsubscribe, only: [ :show ]

Active Recordには、さまざまな目的でデータベースレコードを検索するための一意のトークンを生成できるgenerates_token_forという機能があります。これを使って、電子メールの登録解除用URLに含める一意の登録解除用トークンをSubscriberモデルで生成できます。

class Subscriber < ApplicationRecord
  belongs_to :product
  generates_token_for :unsubscribe
end

コントローラは最初に、URLに含まれるトークンを用いてSubscriberのレコードを検索し、対応する購読者が見つかったら、レコードを破棄(destroy)してホームページにリダイレクトします。

app/controllers/unsubscribes_controller.rbを以下の内容で作成します。

class UnsubscribesController < ApplicationController
  allow_unauthenticated_access
  before_action :set_subscriber

  def show
    @subscriber&.destroy
    redirect_to root_path, notice: "Unsubscribed successfully."
  end

  private

  def set_subscriber
    @subscriber = Subscriber.find_by_token_for(:unsubscribe, params[:token])
  end
end

仕上げに、登録解除用リンクをメールテンプレートに追加しましょう。

app/views/product_mailer/in_stock.html.erbで以下のようにlink_toを追加します。

<h1>Good news!</h1>

<p><%= link_to @product.name, product_url(@product) %> is back in stock.</p>

<%= link_to "Unsubscribe", unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>

app/views/product_mailer/in_stock.text.erbにも以下のように平文でURLを追加します。

Good news!

<%= @product.name %> is back in stock.
<%= product_url(@product) %>

Unsubscribe: <%= unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>

ユーザーがこの登録解除用リンクをクリックすると、データベースからSubscriberのレコードが削除されます。追加したコントローラは、無効なトークンや期限切れのトークンをエラーなしで安全に処理します。

Railsコンソールを起動してメールをもう1件送信し、ログに表示される登録解除リンク(Unsubscribe:で始まります)のURLを見つけてブラウザで開き、正常に登録解除できることをテストしてみてください。成功するとstoreのトップページが開いて「Unsubscribed successfully.」と表示されます。

17 CSSとJavaScriptを追加する

CSSやJavaScriptはWebアプリケーション構築の中心となるため、Railsでの利用方法を学びましょう。

17.1 Propshaft

Railsで、「CSS」「JavaScript」「画像」などのアセットを取得してブラウザに配信するアセットパイプライン(asset pipeline)には、Propshaftが使われています。

production環境のPropshaftは、アセットの各バージョンをトラッキングしてキャッシュすることで、ページを高速化します。アセットパイプラインの仕組みについて詳しくは、アセット パイプライン ガイドを参照してください。

訳注: Rails 8.0からは、従来のSprocketsに代わってPropshaftがデフォルトのアセットパイプラインになりました。

app/assets/stylesheets/application.cssファイルを以下のように更新して、フォントをsans-serifに変更してみましょう。

body {
  font-family: Arial, Helvetica, sans-serif;
  padding: 1rem;
}

nav {
  justify-content: flex-end;
  display: flex;
  font-size: 0.875em;
  gap: 0.5rem;
  max-width: 1024px;
  margin: 0 auto;
  padding: 1rem;
}

nav a {
  display: inline-block;
}

main {
  max-width: 1024px;
  margin: 0 auto;
}

.notice {
  color: green;
}

section.product {
  display: flex;
  gap: 1rem;
  flex-direction: row;
}

section.product img {
  border-radius: 8px;
  flex-basis: 50%;
  max-width: 50%;
}

続いて、app/views/products/show.html.erbファイルを以下のように更新して、新しいスタイルを反映します。

<p><%= link_to "Back", products_path %></p>

<section class="product">
  <%= image_tag @product.featured_image if @product.featured_image.attached? %>

  <section class="product-info">
    <% cache @product do %>
      <h1><%= @product.name %></h1>
      <%= @product.description %>
    <% end %>

    <%= render "inventory", product: @product %>

    <% if authenticated? %>
      <%= link_to "Edit", edit_product_path(@product) %>
      <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
    <% end %>
  </section>
</section>

ブラウザでページを再読み込みすると、CSSが反映されたことを確認できます。

17.2 importmap

RailsのJavaScriptでは、デフォルトでimportmapを経由する形で利用します。これにより、ビルドステップを必要とせずに現代のJavaScriptモジュールを書けるようになります。

利用するJavaScriptパッケージ名は、config/importmap.rbに記述されます。このファイルは、利用するJavaScriptパッケージ名を、ブラウザでimportmapタグを生成するためのソースファイルとpinで対応付けます。

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"

上のpinは、それぞれJavaScriptパッケージ名(例: "@hotwired/turbo-rails")を特定のファイルやURL(例: "turbo.min.js")に対応付けます。pin_all_fromは、ディレクトリ内のすべてのファイル(例: app/javascript/controllers)を名前空間(例: "controllers")に一括で対応付けます。

importmapは、最新のJavaScript機能をサポートすると同時に、JavaScriptのセットアップをクリーンかつ最小限に保ちます。

Railsのconfig/importmap.rbファイルには、既にいくつかのJavaScriptファイルが存在しています。これらは、Hotwireと呼ばれるRailsのデフォルトのフロントエンドフレームワークです。

17.3 Hotwire

Hotwireは、サーバー側で生成されるHTMLを最大限に活用するように設計されたJavaScriptフレームワークで、以下の3つのコアコンポーネントで構成されています。

  1. Turbo: カスタムJavaScriptを記述せずに、ナビゲーションやフォーム送信、ページコンポーネント、更新を処理します。

  2. Stimulus: ページに機能を追加するカスタムJavaScriptが必要な場合のフレームワークを提供します。

  3. Native: Web アプリを埋め込み、ネイティブ モバイル機能で段階的に拡張することで、ハイブリッドモバイル アプリを作成できます。

storeアプリではまだJavaScriptを記述していませんが、storeアプリのフロントエンドでは既にHotwireが動いています。たとえば、製品を追加・編集するために作成したフォームを動かすのに暗黙でTurboが使われています。

詳しくは、アセットパイプラインガイドやRailsでのJavaScript利用ガイドを参照してください。

18 Railsでテストを書く

Railsには堅牢なテストスイートが付属しています。製品の在庫が復活したときに正しい件数のメールが送信されることを確認するテストを記述してみましょう。

18.1 フィクスチャ

Railsでモデルを生成すると、モデルに対応するフィクスチャファイルがtest/fixtures/ディレクトリに自動的に作成されます。

フィクスチャ(fixture)は、テストを実行する前にテストデータベースに取り込まれる定義済みのデータセットです。フィクスチャは、レコードを覚えやすい名前で定義できるため、テストで手軽にアクセスできます。

フィクスチャファイルは、デフォルトでは空なので、テスト用のフィクスチャを取り込む必要があります。

Productモデルのテスト用に、test/fixtures/products.ymlフィクスチャファイルを以下のように更新しましょう。

tshirt:
  name: T-Shirt
  inventory_count: 15

Subscriberモデルのテスト用に、以下の2つのフィクスチャをtest/fixtures/subscribers.ymlに追加します。

david:
  product: tshirt
  email: david@example.org

chris:
  product: tshirt
  email: chris@example.org

このsubscribers.ymlフィクスチャを見ると、tshirtという名前でProduct用フィクスチャを参照できていることがわかります。Railsはこれらをデータベース内で自動的に関連付けるので、レコードIDや関連付けをテストコード内で手動で管理する必要はありません。

これらのフィクスチャは、テストスイートを実行するとき自動でデータベースに挿入されます。

18.2 メール送信をテストする

test/models/product_test.rbに以下のテストを追加してみましょう。

require "test_helper"

class ProductTest < ActiveSupport::TestCase
  include ActionMailer::TestHelper

  test "sends email notifications when back in stock" do
    product = products(:tshirt)

    # 製品を在庫切れにする
    product.update(inventory_count: 0)

    assert_emails 2 do
      product.update(inventory_count: 99)
    end
  end
end

このテストで行っていることを1つずつ詳しく見てみましょう。

最初に、Action Mailer用のテストヘルパーをincludeして、テスト中に送信されたメールを監視できるようにします。

tshirtフィクスチャは フィクスチャが生成するproducts()ヘルパーメソッドで読み込まれ、そのレコードのActive Recordオブジェクトを返します。各フィクスチャはテストスイートでこのようなヘルパーを生成します(データベースIDは実行ごとに異なる可能性があるため、フィクスチャを名前で簡単に参照できるようにします)。

次に、在庫を0に更新して、Tシャツを在庫切れの状態にします。

次に、ブロック内のコードによって2件のメールが生成されたことをassert_emailsアサーションで確認します。

メールをトリガーするには、ブロック内で製品の在庫数を更新して0にします。これにより、Productモデルのnotify_subscribersコールバックがトリガーされ、メールが送信されます。

実行が完了すると、assert_emailsはメールの件数をカウントし、期待される件数と一致することを確認します。

以下のようにファイル名を指定して個別のテストファイルを実行してみましょう。なお、単にbin/rails testコマンドを実行すると、すべてのテストスイートを実行することも可能です。

$ bin/rails test test/models/product_test.rb
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 3556

# Running:

.

Finished in 0.343842s, 2.9083 runs/s, 5.8166 assertions/s.
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips

product_test.rbのテストはパスしました1

ProductMailerを生成したときにも、test/mailers/product_mailer_test.rbにサンプルテストが生成されていますので、こちらも以下のように更新してパスするようにしましょう。

require "test_helper"

class ProductMailerTest < ActionMailer::TestCase
  test "in_stock" do
    mail = ProductMailer.with(product: products(:tshirt), subscriber: subscribers(:david)).in_stock
    assert_equal "In stock", mail.subject
    assert_equal [ "david@example.org" ], mail.to
    assert_equal [ "from@example.com" ], mail.from
    assert_match "Good news!", mail.body.encoded
  end
end

今度は以下のように全テストスイートを実行してみると、すべてのテストがパスすることが確認できます。

$ bin/rails test
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 16302

# Running:

..

Finished in 0.665856s, 3.0037 runs/s, 10.5128 assertions/s.
2 runs, 7 assertions, 0 failures, 0 errors, 0 skips

これらのテストを出発点として、アプリケーション機能を完全にカバーするテストスイートを今後も構築できます。

詳しくは、Railsアプリケーションのテストガイドを参照してください。

19 RuboCopでコードの形式を統一する

コードを書くときのフォーマットが、人や場所によってばらついてしまうことがあります。Railsには、コードのフォーマットを統一するうえで役に立つRuboCopというlinterが付属しています。

以下のコマンドを実行することで、コードが一貫しているかどうかをチェックできます。

$ bin/rubocop

実行すると、違反とその内容が出力されます。 なお、以下は違反が発生していない場合の出力です。

Inspecting 53 files
.....................................................

53 files inspected, no offenses detected

RuboCopのコマンドで以下のように--autocorrect(または短縮形の-a)フラグを追加すると、修正可能な違反が自動修正されます。

$ bin/rubocop -a

20 セキュリティチェック

Railsには、アプリケーションのセキュリティ問題(セッションハイジャック、セッション固定、リダイレクトなどの、攻撃につながる可能性のある脆弱性)をチェックするためのBrakeman gemが含まれています。

bin/brakemanコマンドを実行すると、アプリケーションの脆弱性を分析してレポートを出力します。

$ bin/brakeman
Loading scanner...
...

== Overview ==

Controllers: 6
Models: 6
Templates: 15
Errors: 0
Security Warnings: 0

== Warning Types ==


No warnings found

セキュリティについて詳しくは、Railsアプリケーションのセキュリティガイドを参照してください。

21 GitHub ActionsでCIを実行する

Railsアプリを生成すると、.github/フォルダも生成されます。ここには、rubocopやbrakemanやテストスイートを自動実行するよう事前構成済みのGitHub Actions設定が含まれています。

GitHub Actionsが有効になっているGitHubリポジトリにRailsアプリのコードをプッシュすれば、それだけでGitHub Actionsでこれらの手順が自動的に実行され、項目ごとに成功や失敗を報告してくれます。

これにより、コードの変更を監視して欠陥や問題を検出し、作業の品質を統一的に担保できるようになります。

22 Kamalでproduction環境にデプロイする

いよいよお楽しみが始まります。アプリをデプロイしてみましょう。

Railsに付属しているKamalというデプロイツールを使えば、アプリケーションをサーバーに直接デプロイできます。KamalはアプリケーションをDockerコンテナで実行し、ダウンタイムなしでデプロイします。

Railsには、デフォルトでproduction環境に対応したDockerfileが付属しており、Kamalはこれを使ってDockerイメージをビルドし、コンテナ化されたアプリケーションとそのすべての依存関係と設定を作成します。

なお、このDockerfileでは、production環境でアセットを効果的に圧縮して配信するために、Thrusterを使っています。

Kamalでデプロイを行うには、以下のものが必要です。

  • 1GB以上のRAMを搭載したUbuntu LTSを実行するサーバー。 デプロイ先のサーバーは、定期的なセキュリティとバグ修正を受けられるように、LTS(長期サポート)版のUbuntu OSを実行している必要があります。HetznerやDigitalOceanなどのホスティングサービスでは、Kamalの利用をすぐ開始できるサーバーを提供しています。
  • Docker Hubのアカウントとアクセストークン。 Docker Hubは、アプリケーションのイメージを保存し、サーバーにダウンロードして実行できるようにします。

Docker Hubで、アプリケーションイメージのリポジトリを作成します。リポジトリの名前は「store」にしておきます。

config/deploy.ymlファイルをエディタで開いて、サーバーのIPアドレスを192.168.0.1に置き換え、your-userをDocker Hubのユーザー名に置き換えます。

# Name of your application. Used to uniquely configure containers.
service: store

# Name of the container image.
image: your-user/store

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1

# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  # server: registry.digitalocean.com / ghcr.io / ...
  username: your-user

proxy:セクションでは、アプリケーションでSSLを有効にするためのドメインも追加できます。DNSレコードがサーバーを確実に指していることを確認してください。Kamalは、LetsEncryptを用いてドメインのSSL証明書を発行します。

proxy:
  ssl: true
  host: app.example.com

KamalがアプリケーションのDockerイメージをプッシュできるように、DockerのWebサイトで読み取りと書き込みの権限を持つアクセストークンを作成します。

次に、ターミナルでアクセストークンをKAMAL_REGISTRY_PASSWORD環境変数にexportして、Kamalがアクセストークンを利用できるようにします。

export KAMAL_REGISTRY_PASSWORD=your-access-token

サーバーのセットアップとアプリケーションのデプロイを初めて行うときは、以下のコマンドを実行します。

$ bin/kamal setup

おめでとうございます! 新しいRailsアプリケーションがproduction環境で動くようになりました!

新しいRailsアプリケーションが動いていることを確認してみましょう。ブラウザでサーバーのIPアドレスを入力すると、ストアが動いていることが確認できるはずです。

以後、アプリに変更を加えたら、以下のコマンドを実行するだけでproduction環境にプッシュできます。

$ bin/kamal deploy

22.1 production環境でユーザーを作成する

production環境で製品の作成・編集を行うには、production環境のデータベースにUserのレコードが必要です。

以下のKamalコマンドを使えば、production環境に接続してRailsコンソールを開けます。

$ bin/kamal console
store(prod)> User.create!(email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t")

これで、入力したメールアドレスとパスワードでproduction環境にログインして、製品を管理できるようになります。

22.2 Solid Queueでバックグラウンドジョブを処理する

バックグラウンドジョブを使うと、タスクをバックグラウンドで非同期的に別プロセスで実行できるため、ユーザーエクスペリエンスを損なわずに済みます。

10,000人の受信者に在庫メールを送信することを想像してみましょう。大量のメール送信には時間がかかる可能性があるため、そのタスクをバックグラウンドジョブに乗せ換えることで、Railsアプリの応答性を維持できるようになります。

development環境のRailsでは、:asyncキューアダプタでActiveJobのバックグラウンドジョブを処理します。asyncアダプタは保留中のジョブをメモリに保存しますが、再起動すると保留中のジョブは失われます。これはdevelopment環境には最適ですが、production環境には適していません。

Railsのproduction環境では、バックグラウンドジョブをより堅牢にするためにsolid_queueを利用します。Solid Queueは、ジョブをデータベースに保存して、別プロセスで実行します。

Solid Queueは、config/deploy.ymlSOLID_QUEUE_IN_PUMA: true環境変数によって、production環境でのKamalデプロイで有効になっています。これにより、Puma WebサーバーはSolid Queueプロセスの開始と停止を自動的に行います。

メールがAction Mailerのdeliver_laterメソッドによって送信されると、これらのメールはバックグラウンド送信のためにActive Jobに送信されるので、HTTPリクエストが遅延せずに済みます。production環境でSolid Queueを使うと、メールはバックグラウンドで送信され、送信に失敗した場合に自動的に再試行され、ジョブは再起動中でもデータベースに安全に保持されます。

バックグラウンドジョブの流れ

23 今後のステップ

初めてのRailsアプリケーションの構築、お疲れ様でした。デプロイの完了おめでとうございます!

学習を続けるために、機能を追加してアップデートのデプロイを繰り返してみることをオススメします。アプリケーションの改善案の例を以下に示します。

  • CSSでデザインを改善する
  • 製品レビュー機能を追加する
  • アプリを別の言語に翻訳する
  • 支払い用のチェックアウトフローを追加する
  • ユーザーが製品を保存できるウィッシュリストを追加する
  • 製品画像のカルーセルを追加する

Railsの学習を続けるために、Ruby on Railsの以下のガイドもぜひ参照してみてください。

アプリは楽しく作りましょう!

24 参考資料(日本語)

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加