このガイドでは、Ruby on Rails(以下、Rails)を初めて設定して実行するまでを解説します。
このガイドの内容:
Railsの世界へようこそ! 本ガイド「Railsをはじめよう」では、Railsを活用してWebアプリケーションを構築するときの中核となる概念について解説します。本ガイドを理解するために、Railsの経験は必要ありません。
Railsは、Rubyプログラミング言語用に構築されたWebフレームワークです。RailsはRuby独自のさまざまな機能を活用しているため、このチュートリアルで紹介する基本的な用語や語彙を理解できるように、事前にRubyの基礎を学習しておくことを強くオススメします。
訳注:Railsガイドでは開発経験者が早くキャッチアップできるよう、多くの用語説明を省略しています。読んでいて「難しい」と感じた場合はRailsチュートリアルからお読みください。
Railsとは、プログラミング言語「Ruby」で書かれたWebアプリケーションフレームワークです。Railsは、あらゆる開発者がWebアプリケーション開発で必要となる作業やリソースを事前に想定することで、Webアプリケーションをより手軽に開発できるように設計されています。
Railsは、他の多くのWebアプリケーションフレームワークと比較して、アプリケーションを開発する際のコード量がより少なくて済むにもかかわらず、より多くの機能を実現できます。ベテラン開発者の多くが「RailsのおかげでWebアプリケーション開発がとても楽しくなった」と述べています。
Railsは「最善の開発方法は1つである」という、ある意味大胆な判断に基いて設計されています。何かを行うための最善の方法を1つ仮定して、それに沿った開発を全面的に支援します。言い換えれば、Railsで仮定されていない別の開発手法は行いにくくなります。
この「Rails Way」、すなわち「Railsというレールに乗って開発する」手法を学んだ人は、開発の生産性が驚くほど向上することに気付くでしょう。逆に、レールに乗らずに従来の開発手法にこだわると、開発の楽しさが減ってしまうかもしれません。
Railsの哲学には、以下の2つの主要な基本理念があります。
ここでは、Railsの組み込み機能のいくつかをデモンストレーションするシンプルなeコマースアプリをstoreというプロジェクト名で構築します。
ドル記号$で始まるコマンドは、ターミナルで実行する必要があります。
このプロジェクトでは以下のものが必要です。
RubyやRailsをインストールする必要がある場合は、Ruby on Rails インストールガイドに記載されている手順に従ってください。
訳注:GitHubが提供するクラウド開発環境『Codespaces』の公式Ruby on Railsテンプレートを使うと、数クリックでRailsを動かせるクラウド開発環境が手に入ります。(参考: GitHub Codespacesを利用する - Railsチュートリアル)
正しいバージョンのRailsがインストールされていることを確認しておきましょう。現在のバージョンを表示するには、ターミナルを開いて以下のコマンドを実行すると、バージョン番号が出力されます。
$ rails --version Rails 8.1.0
バージョン番号はRails 8.1.0以降になるはずです。
Railsには、作業を楽にするためのさまざまなコマンドが付属しています。
利用可能なコマンドをすべて表示するには、rails --helpを実行します。
rails newコマンドは、新しいRailsアプリケーションの基盤を生成するので、まずこのコマンドを実行することから始めましょう。
storeアプリケーションを作成するには、ターミナルで以下のコマンドを実行します。
$ rails new store
rails newコマンドにフラグを追加すると、Railsが生成するアプリケーションをカスタマイズできます。利用可能なオプションをすべて表示するには、rails new --helpを実行します。
新しいアプリケーションを作成したら、そのディレクトリに移動します。
$ cd store
新しい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バージョンが記述されています。 |
Railsのコードは、MVC(Model-View-Controller)アーキテクチャに基づいて編成されています。MVCでは、コードの大部分が以下の3つの主要な概念に基づいて配置されます。

MVCの基本部分を理解したので、MVCがどのようにRailsで使われるかを見てみましょう。
それでは、Railsのデータベースを作成して、Railsサーバーを初めて起動してみましょう。
ターミナルでstoreディレクトリに移動し、以下のコマンドを実行します。
$ bin/rails db:create
これで、アプリケーションの初期データベースが作成されました。 続いて、Railsサーバーを起動します。
$ bin/rails server
Railsコマンドをアプリケーションディレクトリ内で実行するときは、単独のrailsよりもbin/railsを使うようにしましょう。これにより、そのアプリケーションで使われているRailsのバージョンが確実に使われます。
すると、PumaというWebサーバーが起動します。Pumaサーバーは、静的ファイルやRailsアプリケーションの配信を担当します。
=> Booting Puma => Rails 8.1.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アプリケーションの「スモークテスト」として使えます。このページが表示されれば、サーバーが正常に動作していることが確認できます。
実行されているターミナルのウィンドウでCtrl + Cキーを押せば、いつでもWebサーバーを停止できます。
開発者の幸福はRailsの基本的な哲学であり、開発中にコードを自動で再読み込みする機能は、これを実現する方法の1つです。
Railsサーバーを起動すると、新しいファイルや既存のファイルへの変更が検出され、実行中も必要に応じてコードの読み込みや再読み込みが自動的に行われます。これにより、コード変更のたびにRailsサーバーを再起動しなくても済むので、アプリの構築に集中できます。
また、Railsアプリケーションでは、他のプログラミング言語で見られるようなrequireステートメントがほとんど使われていないことにも気付くでしょう。 Railsでは命名規則に基づいてファイルを自動的にrequireするので、アプリケーションコードの記述に集中できます。
詳しくは別ガイド『Railsの自動読み込みと再読み込み』を参照してください。
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
このコマンドは以下を行います。
db/migrateフォルダの下にマイグレーションファイルを作成。app/models/product.rbというActive Recordモデルを作成。Railsのモデル名には英語の単数形を使います。これは、インスタンス化されたモデルはデータベース内の1件のレコードを表す(データベースに1個の製品(a product)を追加する)という考えに基づいています。
マイグレーション(migration)とは、データベースに対して行う一連の変更のことです。
マイグレーションを定義することで、データベースのテーブルやカラム、およびその他の属性を追加・変更・削除するためにデータベースを変更する方法を統一された形でRailsに指示します。 これにより、自分のコンピュータ上での開発中に行ったデータベース変更をトラッキングして、production環境に安全にデプロイできるようにします。
Railsが作成したマイグレーションをコードエディタで開いて、マイグレーションで何が行われるかを確認してみましょう。マイグレーションファイルはdb/migrate/<タイムスタンプ>_create_products.rbに配置されます。
class CreateProducts < ActiveRecord::Migration[8.1] 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:datetimeとupdated_at:datetimeの2つのカラムを一度に定義するショートカットです。
これらのカラムは、RailsのほとんどのActive Recordモデルで表示され、レコードの作成時や更新時にActive Recordによって自動的に値が設定されます。
データベースに対して行う変更を定義したら、以下のコマンドを使ってマイグレーションを実行します。
$ 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を実行することで直前のマイグレーションに戻せます。
productsテーブルが作成されたので、Railsで操作できるようになりました。
さっそく試してみましょう。
ここでは、Railsコンソールと呼ばれる機能を使います。Railsコンソールは、Railsアプリケーションでコードを試すときに便利な対話型ツールです。
$ bin/rails console
上のRailsコンソールコマンドを実行すると、以下のようなプロンプトが表示されます。
Loading development environment (Rails 8.1.0) store(dev)>
ここで入力した内容は、Enterを押すと実行されます。
それではRailsバージョンを出力してみましょう。
store(dev)> Rails.version => "8.1.0"
たしかに動きました!
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を使うことで開発がいかに簡単になるかを示す一例です。
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モデルのインスタンスです。この時点ではまだデータベースに保存されていないため、id、created_atとupdated_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">
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オブジェクトは、配列に似たデータベースレコードのコレクションで、フィルタリングや並べ替えなどのデータベース操作を実行する機能を備えています。
データベースから受け取った結果をフィルタで絞り込みたい場合は、以下のように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">]
特定のレコードを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モデルのインスタンスを取得します。
レコードを更新するには、「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
データベースからレコードを削除するには、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">]
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属性が指定されていないため、saveはfalseを返します。
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を実行してコンソールを終了できます。
Railsで「Hello」を表示するには、少なくとも「ルーティング」と「コントローラ」、そしてコントローラに付随する「アクション」と「ビュー」を作成する必要があります。
これらは、実装の観点では以下のようになります。
ルーティングはRubyのDSL(ドメイン固有言語)で記述されたルールです。 コントローラは普通のRubyクラスであり、そのpublicメソッドがアクションになります。 ビューはテンプレートであり、通常はHTMLとRubyを組み合わせて記述されます。
以上はごく簡単な説明ですが、次にこれらの各ステップについてさらに詳しく説明します。
Railsのルーティング(route、routing)はURLを構成する要素の1つであり、受信したHTTPリクエストを適切なコントローラとアクションに転送することでリクエストの処理方法を決定します。
まず、URLとHTTPリクエストメソッドについて簡単に復習しましょう。
URLがどのような要素から構成されているかを詳しく見てみましょう。
https://example.org/products?sale=true&sort=asc
上のURLの各要素には、以下のような名前があります。
httpsの部分はプロトコル(protocol)と呼ばれますexample.orgの部分はホスト(host)と呼ばれます/productsの部分はパス(path)と呼ばれます?sale=true&sort=ascの部分はクエリパラメータ(query parameters)と呼ばれますHTTPリクエストは、特定のURLに対してサーバーが実行すべきアクションを指示するときにHTTPメソッド(HTTP verb: HTTP動詞とも呼ばれます)を利用します。
最も一般的なHTTPメソッドは次のとおりです。
GETリクエスト:
特定のURLのデータを取得するようサーバーに指示します(ページの読み込みやレコードの取得など)。POSTリクエスト:
処理を実行するためのデータをURLに送信します(通常は新しいレコードを作成します)。PUTまたはPATCHリクエスト:
既存のレコードを更新するためのデータをURLに送信します。DELETEリクエスト:
URLに送信されると、レコードを削除するようサーバーに指示します。Railsにおけるルーティングは、HTTPメソッドとURLパスをペアにしたコード行を指します。
ルーティングは、どのcontrollerとactionでリクエストに応答すべきかを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がリクエストに適用できるオプションのようなもので必須ではなく、通常はコントローラでデータをフィルタリングするときに使われます。

別の例も見てみましょう。 前述のルーティングの下に、以下の行を追加します。
post "/products", to: "products#create"
ここでは、/productsパスへのPOSTリクエストを受け取ったら、ProductsControllerのcreateアクションでリクエストを処理するよう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"
リソースへの操作で通常必要となる一般的な操作は、「作成」「読み取り」「更新」「削除」の4つであり、CRUDと呼ばれます。
これは、8つの一般的なコントローラアクションに相当します。
index: すべてのレコードを表示しますnew: 新しいレコード1件を作成するためのフォームをレンダリングしますcreate: newのフォーム送信を処理し、エラーを処理してレコードを1件作成しますshow: 指定のレコード1件をレンダリングして表示しますedit: 指定のレコード1件を更新するためのフォームをレンダリングしますupdate(リソース全体): リソース全体を更新するフォーム送信を処理します。
これは通常、PUTリクエストでトリガーされ、リソースのすべての属性を置き換えますupdate(特定のリソースのみ): 特定の属性のみを更新するフォーム送信を処理します。
これは通常、PATCHリクエストでトリガーされ、リソースを部分的に更新します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"
これら8つのルーティングを毎回入力するのは冗長なので、Railsではルーティングを1行で定義できるショートカットを提供しています。
上記のルーティングを以下の1行に置き換えて、上と同じCRUDアクションをすべて作成できるようにしましょう。
resources :products
CRUDアクションの一部しか使わない場合は、必要なアクションだけを正確に指定し、使わないアクションは無効にしておきましょう。詳しくはルーティングガイドを参照してください。
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にブラウザでアクセスすることでルーティング情報を表示できます。
製品のルーティングを定義したので、次はコントローラとアクションを実装して、これらの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>
作成した結果をブラウザで確認してみましょう。
まず、ターミナルでbin/rails serverを実行してRailsサーバーを起動します。
次に、ブラウザでhttp://localhost:3000を開くと、Railsのウェルカムページが表示されます。
ブラウザでhttp://localhost:3000/productsを開くと、Railsは製品のindexページのHTMLをレンダリングします。
このときの処理の流れは以下のようになります。
/productsパスへのリクエストを送信すると、ルーティングはproducts#indexにマッチします。ProductsControllerに送信してindexアクションを呼び出します。indexアクションは空なので、Railsはこのコントローラアクションに一致するapp/views/products/index.html.erbテンプレートをレンダリングして、ブラウザにレスポンスを返します。なお、config/routes.rbファイルに以下の行を追加すると、rootパスにアクセスしたときのルーティングでProductsのindexアクションをレンダリングするようにRailsに指示できます。
root "products#index"
これで、http://localhost:3000にアクセスすると、RailsがProducts#indexをレンダリングするようになります。
さらに先に進んで、データベースにあるレコードをいくつかレンダリングしてみましょう。
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の出力が無視されるようになります。
次は、個別の製品を1件ずつ表示できるようにする必要があります。これは、リソースを読み取るためのCRUDのR(Read)に相当します。
製品へのルーティングは、既にresources :productsルーティングでまとめて定義してあるので、products#showを指すルーティングとして/products/:id が生成されるようになっています。
次に、これに対応するshowアクションをProductsControllerに追加して、呼び出されたときの振る舞いを定義する必要があります。
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/rails routesを実行すると以下のように表示されるPrefix列のproductsやproductは、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_path(product.id) %> </div> <% end %> </div>
これまでは製品をRailsコンソールで作成するしかありませんでしたが、今度はブラウザで製品を作成できるようにしましょう。
製品を作成するには、以下の2つのアクションを作成する必要があります。
newアクション: 製品情報を収集するためのフォームを作成する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_path(product.id) %> </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 %> <%= link_to "Cancel", products_path %>
この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
createアクションはフォームから送信されたデータを処理しますが、セキュリティのためにパラメータをフィルタリングしておかなければなりません。ここで役に立つのが、privateメソッドとして追加したproduct_paramsメソッドです。
訳注: メソッド名のproductの部分はモデル名と同じ単数形にするのが慣例です)。
product_paramsメソッドは、リクエストで受け取ったパラメータを検査して、パラメータの配列を値として持つ:productというキーが必ず存在することを保証します。ここでは、製品に許可されているパラメータは:nameのみなので、これ以外のどんなパラメータをRailsに渡しても無視されます(エラーにはなりません)。これにより、アプリケーションをハッキングしようとする悪意のあるユーザーからアプリケーションが保護されます。
詳しくはStrong Parameterを参照してください。
product_paramsを使ってこれらのパラメータを新しいProductに割り当てたら、データベースへの保存を試みる準備が整います。@product.saveは、バリデーションを実行してレコードをデータベースに保存するようActive Recordに指示します。
saveが成功すると、新しい製品のshowページにリダイレクトします。redirect_toに Active Recordオブジェクトを渡すと、そのレコードのshowアクションへのパスが生成されます。
redirect_to @product
上を実行すると、@productはProductモデルのインスタンスなので、リダイレクト用に"/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リクエストが失敗したことを伝えて、それに応じた処理に備えます。
レコードを編集する処理は、レコードを作成する処理と非常に似ています。レコード作成では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
新しい製品を作成するためのフォームは既に作成しましたが、このフォームを編集や更新のフォームでも再利用できたら便利だと思いませんか?これは、複数の場所でビューを再利用できるようにするパーシャル(partial)という機能を使ってフォームをapp/views/products/_form.html.erbというパーシャルファイルに切り出すことで実現できます。
パーシャルのファイル名は、これがパーシャルであることを示すためにアンダースコア_で始まります。
それと同時に、ビューで使われているインスタンス変数をすべてローカル変数に置き換えたいと思います。ローカル変数は、パーシャルをレンダリングするときに定義できます。これを行うには、パーシャル内の@productを以下のようにproductに置き換えます。このとき、フォーム送信のエラーメッセージもフォームの一部として表示できるようにしておきます。
<%= form_with model: product do |form| %> <% if form.object.errors.any? %> <p class="error"><%= form.object.errors.full_messages.first %></p> <% end %> <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ガイドを参照してください。
次に、app/views/products/show.html.erbビューテンプレートにEditページへのリンクを追加します。
<h1><%= @product.name %></h1> <%= link_to "Back", products_path %> <%= link_to "Edit", edit_product_path(@product) %>
before_actionコールバックでコードをDRYにするeditアクションとupdateアクションは、showと同様に既存のデータベースレコードが存在している必要があります。before_actionを使うことで、そのための同じコードの重複を排除できます。
before_actionを使うと、アクション間で共有されているコードを抽出して、アクションの直前に実行できます。
上のコントローラでは、@product = Product.find(params[:id])という同じコードがshow、edit、updateという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
実装が必要な最後の機能は、製品の削除です。
DELETE /products/:idリクエストを処理するために、ProductsControllerにdestroyアクションを追加しましょう。
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/:idにDELETEリクエストとして送信され、コントローラのdestroyアクションがトリガーされます。
なお、turbo_confirmデータ属性は、フォームを送信する前にユーザーに確認ダイアログを表示するようにTurboというJavaScriptライブラリに指示します。これについては、後ほど詳しく説明します。
今のままでは誰でも製品を編集・削除できてしまうので、安全ではありません。製品管理でユーザー認証を必須にすることで、セキュリティを強化しましょう。
ここでは、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(認証情報)を保存するので、ページビューを移動するたびに入力する必要はありません。
アプリケーションからログアウトするためのボタンを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_pathにDELETEリクエストが送信されて、ユーザーがログアウトします。
ただし、ストアの製品indexページとshowページは誰でもアクセスできるようにしておく必要があります。Railsの認証ジェネレータは、デフォルトではすべてのページへのアクセスを認証済みユーザーのみに制限します。
ゲストが製品を表示できるようにするには、コントローラで以下のように認証なしのアクセスを許可します。
class ProductsController < ApplicationController allow_unauthenticated_access only: %i[ index show ] # (省略) end
ログアウトしてから再び製品のindexページとshowページにアクセスし、認証なしでアクセスできるかどうかを確認してみてください。
製品を作成してよいのはログイン済みのユーザーだけにしておきたいので、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リンクとDeleteリンクを以下のように更新して、認証済みの場合にのみEditリンクとDeleteリンクを表示するようにしてもよいでしょう。
<h1><%= @product.name %></h1> <%= link_to "Back", products_path %> <% if authenticated? %> <%= link_to "Edit", edit_product_path(@product) %> <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %> <% end %>
ページの特定の部分をキャッシュすると、パフォーマンスが向上する場合があります。 Railsは、データベース上に構築されるキャッシュストアであるSolid Cacheをデフォルトで組み込むことで、このプロセスを簡素化しています。
cacheメソッドを使うと、HTMLをキャッシュに保存できます。app/views/products/show.html.erbの<h1>見出しを以下のように囲んで、キャッシュを有効にしてみましょう。
<% cache @product do %> <h1><%= @product.name %></h1> <% end %>
@productをcacheメソッドに渡すと、製品に固有のキャッシュキーを生成します。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 のキャッシュガイドを参照してください。
リッチテキスト機能やマルチメディア要素の埋め込み機能は、多くのアプリケーションで求められています。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_textarea :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の概要を参照してください。
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 の概要を参照してください。
Railsを使えば、アプリを他の言語に翻訳しやすくなります。
ビューのtranslateヘルパー(短縮形はt)は、名前で訳文を検索して、現在のロケール設定に合うテキストを返します。
app/views/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では、以下のようにproductsとindexを追加し、その下に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ガイドを参照してください。
製品の在庫が復活したときに通知を受け取るための電子メールを登録する機能は、eコマースストアでよく使われる機能です。Railsの基本についてひととおり確認したので、今度はこの機能をストアに追加してみましょう。
まず、Productモデルにinventory_count(在庫数)を追加して、在庫数をトラッキングできるようにしましょう。以下のコマンドを実行してマイグレーションを生成します。
$ bin/rails generate migration AddInventoryCountToProducts inventory_count:integer
これでマイグレーションファイルが生成されます。マイグレーションファイルを開いて、inventory_countが決してnilにならないようにするために、デフォルト値として0を追加します。
class AddInventoryCountToProducts < ActiveRecord::Migration[8.1] def change add_column :products, :inventory_count, :integer, default: 0 end end
続いて以下のコマンドでマイグレーションを実行します。
$ 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
以上の変更により、ストア内の製品の在庫数を更新できるようになりました。
商品の在庫が復活したことをユーザーに通知するには、在庫情報の購読者(subscriber)をトラッキングする機能が必要です。
購読希望者のメールアドレスを保存して個別の商品に関連付けるためのSubscriberというモデルを生成しましょう。
ここではemailフィールドに型を指定していませんが、Railsのマイグレーションで型が指定されていない場合、自動的にstring型がデフォルトになることを利用しています。
$ bin/rails generate model Subscriber product:belongs_to email
上のコマンドでproduct:belongs_toオプションを指定したことで、購読者と製品が1対多リレーションを持つことを表すbelongs_to :productという宣言がSubscriberモデルに含まれるようになります。つまり、Subscriberモデルのインスタンスは1つのProductインスタンスに「属する(belongs to)」ということです。
次に、生成されたマイグレーションファイル(db/migrate/<timestamp>_create_subscribers.rb)をエディタで開きます。
class CreateSubscribers < ActiveRecord::Migration[8.1] def change create_table :subscribers do |t| t.belongs_to :product, null: false, foreign_key: true t.string :email t.timestamps end end end
このマイグレーションは、Productのマイグレーションと非常に似ています。主な新しい点はbelongs_toが含まれていることで、これによりproduct_idという外部キーカラムが追加されます。
続いて新しいマイグレーションを実行します。
$ bin/rails db:migrate
ただし、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
redirect_toメソッドのnotice:引数には、購読が完了したことを知らせるflashメッセージを指定しています。
flashは、コントローラのアクション間で一時的なデータを渡す手段を提供します。flashに設定したデータは次のアクションで有効になり、その後消去されます。flashは通常、コントローラのアクションでメッセージ(通知やアラートなど)を設定し、その後、ユーザーにメッセージを表示するアクションにリダイレクトするために使われます。
このflashメッセージを表示するには、app/views/layouts/application.html.erbレイアウトの<body>タグで以下のように通知を追加します。
<html> <!-- (省略) --> <body> <div class="notice"><%= flash[:notice] %></div> <div class="alert"><%= flash[:alert] %></div> <!-- (省略) --> </body> </html>
flashについて詳しくは、Action Controllerガイドを参照してください。
ユーザーが特定の製品を指定して通知を購読できるようにするため、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.positive? %> <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.erbのcacheブロックの下に以下のコードを追加して、上のパーシャルをレンダリングします。
<%= render "inventory", product: @product %>
商品の在庫が復活したときに購読者に通知する機能には、Railsのメール送信機能であるAction Mailerを使うことにします。
以下のコマンドを実行することでメーラーを生成できます。
$ bin/rails g mailer Product in_stock
これにより、app/mailers/product_mailer.rbにin_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_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 } after_update_commit :notify_subscribers, if: :back_in_stock? def back_in_stock? inventory_count_previously_was.zero? && inventory_count.positive? 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_was(inventory_count属性から自動生成された属性名_previously_wasメソッド)を使っています。続いて、その値を現在の在庫数と比較して、製品の在庫が復活したかどうかを判断します。
notify_subscribersは、特定の製品のすべての購読者のリストを得るためにActive Record関連付けを利用してsubscribersテーブルを照会してから、個別の購読者に送信するin_stockメールをキューに登録します。
この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.zero? && inventory_count.positive? 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_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に抽出すると、機能の再利用性も高まります。たとえば、サブスクライバ通知も必要とする新しいモデルを手軽に導入できるようになります。このモジュールを利用する複数のモデルで同じ機能を提供できます。
通知を購読したユーザーが購読を解除できるようにしておきたいので、次は購読の解除機能を構築することにしましょう。
最初に、config/routes.rbのルーティングに購読解除用のルーティングを追加する必要があります。これは解除用メールに含めるURLとして使われます。
Rails.application.routes.draw do # ... resources :products do resources :subscribers, only: [ :create ] end resource :unsubscribe, only: [ :show ]
購読解除用のルーティングは最上位レベルに追加します。/unsubscribe?token=xyzのようなルーティングを処理するために、単数形のresourceメソッドを使っているにご注意ください。
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.」と表示されます。
CSSやJavaScriptはWebアプリケーション構築の中心となるため、Railsでの利用方法を学びましょう。
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;
}
.alert,
.error {
color: red;
}
.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が反映されたことを確認できます。
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 "trix" pin "@rails/actiontext", to: "actiontext.esm.js"
上の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のデフォルトのフロントエンドフレームワークです。
Hotwireは、サーバー側で生成されるHTMLを最大限に活用するように設計されたJavaScriptフレームワークで、以下の3つのコアコンポーネントで構成されています。
Turbo: カスタムJavaScriptを記述せずに、ナビゲーションやフォーム送信、ページコンポーネント、更新を処理します。
Stimulus: ページに機能を追加するカスタムJavaScriptが必要な場合のフレームワークを提供します。
Native: Web アプリを埋め込み、ネイティブ モバイル機能で段階的に拡張することで、ハイブリッドモバイル アプリを作成できます。
storeアプリではまだJavaScriptを記述していませんが、storeアプリのフロントエンドでは既にHotwireが動いています。たとえば、製品を追加・編集するために作成したフォームを動かすのに暗黙でTurboが使われています。
詳しくは、アセットパイプラインガイドやRailsでのJavaScript利用ガイドを参照してください。
Railsには堅牢なテストスイートが付属しています。製品の在庫が復活したときに正しい件数のメールが送信されることを確認するテストを記述してみましょう。
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や関連付けをテストコード内で手動で管理する必要はありません。
これらのフィクスチャは、テストスイートを実行するとき自動でデータベースに挿入されます。
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のテストはパスしました
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アプリケーションのテストガイドを参照してください。
コードを書くときのフォーマットが、人や場所によってばらついてしまうことがあります。Railsには、コードのフォーマットを統一するうえで役に立つRuboCopというlinterが付属しています。
以下のコマンドを実行することで、コードが一貫しているかどうかをチェックできます。
$ bin/rubocop
実行すると、違反とその内容が出力されます。 なお、以下は違反が発生していない場合の出力です。
Inspecting 53 files ..................................................... 53 files inspected, no offenses detected
RuboCopのコマンドで以下のように--autocorrect(または短縮形の-a)フラグを追加すると、修正可能な違反が自動修正されます。
$ bin/rubocop -a
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アプリケーションのセキュリティガイドを参照してください。
Railsアプリを生成すると、.github/フォルダも生成されます。ここには、rubocopやbrakemanやテストスイートを自動実行するよう事前構成済みのGitHub Actions設定が含まれています。
GitHub Actionsが有効になっているGitHubリポジトリにRailsアプリのコードをプッシュすれば、それだけでGitHub Actionsでこれらの手順が自動的に実行され、項目ごとに成功や失敗を報告してくれます。
これにより、コードの変更を監視して欠陥や問題を検出し、作業の品質を統一的に担保できるようになります。
いよいよお楽しみが始まります。アプリをデプロイしてみましょう。
Railsに付属しているKamalというデプロイツールを使えば、アプリケーションをサーバーに直接デプロイできます。KamalはアプリケーションをDockerコンテナで実行し、ダウンタイムなしでデプロイします。
Railsには、デフォルトでproduction環境に対応したDockerfileが付属しており、Kamalはこれを使ってDockerイメージをビルドし、コンテナ化されたアプリケーションとそのすべての依存関係と設定を作成します。
なお、このDockerfileでは、production環境でアセットを効果的に圧縮して配信するために、Thrusterを使っています。
Kamalでデプロイを行うには、以下のものが必要です。
Docker Hubで、アプリケーションイメージのリポジトリを作成します。リポジトリの名前は「store」にしておきます。
config/deploy.ymlファイルをエディタで開いて、192.168.0.1をサーバーのIPアドレスに置き換え、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
production環境で製品の作成・編集を行うには、production環境のデータベースにUserのレコードが必要です。
以下のKamalコマンドを使えば、production環境に接続してRailsコンソールを開けます。
$ bin/kamal console
store(prod)> User.create!(email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t")
これで、入力したメールアドレスとパスワードでproduction環境にログインして、製品を管理できるようになります。
バックグラウンドジョブを使うと、タスクをバックグラウンドで非同期的に別プロセスで実行できるため、ユーザーエクスペリエンスを損なわずに済みます。
10,000人の受信者に在庫メールを送信することを想像してみましょう。大量のメール送信には時間がかかる可能性があるため、そのタスクをバックグラウンドジョブに乗せ換えることで、Railsアプリの応答性を維持できるようになります。
development環境のRailsでは、:asyncキューアダプタでActiveJobのバックグラウンドジョブを処理します。asyncアダプタは保留中のジョブをメモリに保存しますが、再起動すると保留中のジョブは失われます。これはdevelopment環境には最適ですが、production環境には適していません。
Railsのproduction環境では、バックグラウンドジョブをより堅牢にするためにsolid_queueを利用します。Solid Queueは、ジョブをデータベースに保存して、別プロセスで実行します。
Solid Queueは、config/deploy.ymlのSOLID_QUEUE_IN_PUMA: true環境変数によって、production環境でのKamalデプロイで有効になっています。これにより、Puma WebサーバーはSolid Queueプロセスの開始と停止を自動的に行います。
メールがAction Mailerのdeliver_laterメソッドによって送信されると、これらのメールはバックグラウンド送信のためにActive Jobに送信されるので、HTTPリクエストが遅延せずに済みます。production環境でSolid Queueを使うと、メールはバックグラウンドで送信され、送信に失敗した場合に自動的に再試行され、ジョブは再起動中でもデータベースに安全に保持されます。

初めてのRailsアプリケーションの構築、お疲れ様でした。デプロイの完了おめでとうございます!
次は、演習: サインアップと設定チュートリアルに従って学習を続けてください。
Railsの学習を続けるために、Ruby on Railsの以下のガイドもぜひ参照してみてください。
アプリは楽しく作りましょう!
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。