Rails をはじめよう

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

このガイドの内容:

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

1 本ガイドの前提条件

本ガイドは、Railsアプリケーションを構築したいと考えているRails初心者を対象にしています。読者にRailsの経験があることは前提としていません。

Railsとは、プログラミング言語「Ruby」の上で動作するWebアプリケーションフレームワークです。ただしプログラミング経験がまったくない人がいきなりRailsを学ぼうとすると、かなり大変な作業になるでしょう。オンラインで学べる洗練されたコンテンツはたくさんあるので、その中から以下をご紹介します。

いずれもよくできていますが中には古いものもあり、たとえば通常のRails開発で見かけるような最新の構文がカバーされていない可能性もあります。

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

2 Railsとは何か

Railsとは、プログラミング言語「Ruby」で書かれたWebアプリケーションフレームワークです。Railsは、あらゆる開発者がWebアプリケーション開発で必要となる作業やリソースを事前に想定することで、Webアプリケーションをより手軽に開発できるように設計されています。他の多くの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プロジェクトを新規作成する

本ガイドを最大限に活用するには、以下の手順を1つずつすべて実行するのがベストです。どの手順もサンプルアプリケーションを動かすのに必要なものであり、それ以外のコードや手順は不要です。

本ガイドの手順に沿って作業すれば、blogという名前の非常にシンプルなブログのRailsプロジェクトを作成できます。Railsアプリケーションを構築する前に、Rails本体をインストールしておいてください。

以下の例では、Unix系OSのプロンプトとして$記号が使われていますが、プロンプトはカスタマイズ可能なので環境によって異なることもあります。Windowsではc:\source_code>のように表示されます。

3.1 Railsのインストール

Railsをインストールする前に、必要な要件が自分のシステムで満たされているかどうかをチェックしましょう。少なくとも以下のソフトウェアが必要です。

  • Ruby
  • SQLite3

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

3.1.1 Rubyをインストールする

ターミナル(コマンドプロンプトとも言います)ウィンドウを開いてください。macOSの場合、ターミナル(Terminal.app)という名前のアプリケーションを実行します。Windowsの場合は[スタート]メニューから[ファイル名を指定して実行]をクリックして'cmd.exe'と入力します。$で始まる記述はコマンド行なので、これらをコマンドラインに入力して実行します。次に以下を実行して、現在インストールされているRubyが最新バージョンであることを確認しましょう。

$ ruby -v
ruby 3.0.1

RailsではRubyバージョン2.7.0以降が必須です。これより低いバージョン(2.3.7や1.8.7など)が表示された場合は、新たにRubyをインストールする必要があります。

RailsをWindowsにインストールする場合は、最初にRuby Installerをインストールしておく必要があります。

OS環境ごとのインストール方法について詳しくは、ruby-lang.orgを参照してください。

3.1.2 SQLite3をインストールする

SQLite3データベースのインストールも必要です。 多くのUnix系OSには実用的なバージョンのSQLite3が同梱されています。Windowsの場合は、上述のRails InstalerでRailsをインストールするとSQLite3もインストールされます。その他の環境についてはSQLite3のインストール方法を参照してください。

$ sqlite3 --version

上を実行することでSQLite3のバージョンを確認できます。

3.1.3 Railsをインストールする

Railsをインストールするには、gem installコマンドを実行します。このコマンドはRubyGemsによって提供されます。

$ gem install rails

以下のコマンドを実行することで、すべて正常にインストールできたかどうかを確認できます。

$ rails --version
Rails 7.1.0

"Rails 7.1.0"などのバージョンが表示されたら、次に進みましょう。

3.2 ブログアプリケーションを作成する

Railsには、ジェネレータというスクリプトが多数付属していて、特定のタスクを開始するために必要なものを自動的に生成してくれるので、楽に開発できます。その中から、新規アプリケーション作成用のジェネレータを使ってみましょう。ジェネレータを実行すればRailsアプリケーションの基本的なパーツが提供されるので、開発者が自分でこれらを作成する必要はありません。

ジェネレータを実行するには、ターミナルを開き、Railsファイルを作成したいディレクトリに移動して、以下を入力します。

$ rails new blog

これにより、Blogという名前のRails アプリケーションがblogディレクトリに作成され、Gemfileというファイルで指定されているgemファイルがbundle installコマンドによってインストールされます。

rails new --helpを実行すると、Railsアプリケーションビルダで使えるすべてのコマンドラインオプションを表示できます。

ブログアプリケーションを作成したら、そのフォルダ内に移動します。

$ cd blog

blogディレクトリの下には多数のファイルやフォルダが生成されており、これらがRailsアプリケーションを構成しています。このガイドではほとんどの作業をappディレクトリで行いますが、Railsが生成したファイルとフォルダについてここで簡単に説明しておきます。

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

4 Hello, Rails!

手始めに、画面に何かテキストを表示してみましょう。そのためには、Railsアプリケーションサーバーを起動しなくてはなりません。

4.1 Webサーバーを起動する

先ほど作成したRailsアプリケーションは、既に実行可能な状態になっています。Webアプリケーションを開発用のPCで実際に動かしてこのことを確かめてみましょう。blogディレクトリに移動し、以下のコマンドを実行します。

$ bin/rails server

Windowsの場合は、binフォルダの下にあるスクリプトをRubyインタープリタに直接渡さなければなりません(例: ruby bin\rails server

JavaScriptアセットの圧縮にはJavaScriptランタイムが必要です。JavaScriptランタイムが環境にない場合は、起動時にexecjsエラーが発生します。macOSやWindowsにはJavaScriptランタイムが同梱されています。therubyrhinoはJRubyユーザー向けに推奨されているランタイムであり、JRuby環境下ではデフォルトでアプリケーションのGemfileに追加されます。サポートされているランタイムについて詳しくはExecJSを参照してください。

Railsで起動されるWebサーバーは、Railsにデフォルトで付属しているPumaです。Webアプリケーションが実際に動作しているところを確認するには、ブラウザを開いて http://localhost:3000 を表示してください。以下のようなRailsのデフォルト情報ページが表示されます。

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

Webサーバーを停止するには、実行されているターミナルのウィンドウでCtrl + Cキーを押します。なお、development環境ではファイルに変更を加えればサーバーが自動的に変更を反映するので、サーバーの再起動は通常は不要です。

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

4.2 Railsで「Hello」と表示する

Railsで「Hello」と表示するには、少なくとも「ルーティング」「コントローラ」「ビュー」が必要です。ルーティングは、リクエストをどのコントローラに振り分けるかを決定します。コントローラは、アプリケーションに対する特定のリクエストを受け取って処理します。コントローラの アクション は、リクエストを扱うのに必要な処理を実行します。ビューは、データを好みの書式で表示します。

実装の面から見れば、ルーティングはRubyのDSL(Domain-Specific Language) で記述されたルールです。コントローラはRubyのクラスで、そのクラスのpublicメソッドがアクションです。ビューはテンプレートで、多くの場合HTMLの中にRubyコードも含んでいます。

それではルーティングを1個追加してみましょう。config/routes.rbを開き、Rails.application.routes.drawブロックの冒頭に以下を書きます。

Rails.application.routes.draw do
  get "/articles", to: "articles#index"
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # ...
end

上で宣言したルーティングは、GET /articlesリクエストをArticlesControllerindexアクションに対応付けます。

ArticlesControllerindexアクションを作成するには、コントローラ用のジェネレータを実行します(上で既に適切なルーティングを追加したので、ここでは--skip-routesオプションでルーティングの追加をスキップします)。

$ bin/rails generate controller Articles index --skip-routes

これで、必要なファイルをRailsが生成します。

create  app/controllers/articles_controller.rb
invoke  erb
create    app/views/articles
create    app/views/articles/index.html.erb
invoke  test_unit
create    test/controllers/articles_controller_test.rb
invoke  helper
create    app/helpers/articles_helper.rb
invoke    test_unit

この中で最も重要なのは、app/controllers/articles_controller.rbというコントローラファイルです。このファイルを見てみましょう。

class ArticlesController < ApplicationController
  def index
  end
end

indexアクションは空です。あるアクションがビューを明示的にレンダリングしない場合(またはHTTPレスポンスをトリガーしない場合)、Railsはその「コントローラ名」と「アクション名」にマッチするビューを自動的にレンダリングします。これは「設定より規約」の例です。ビューはapp/viewsの下に配置されるので、indexアクションはデフォルトでapp/views/articles/index.html.erbをレンダリングします。

それではapp/views/articles/index.html.erbを開き、中身を以下に置き換えましょう。

<h1>Hello, Rails!</h1>

コントローラ用のジェネレータを実行するためにWebサーバーを停止していた場合は、再びbin/rails serverを実行します。ブラウザでhttp://localhost:3000/articlesを開くと、「Hello, Rails!」というテキストが表示されます。

4.3 アプリケーションのHomeページを設定する

この時点ではトップページhttp://localhost:3000にまだRailsのデフォルト起動画面が表示されているので、http://localhost:3000を開いたときにも「Hello, Rails!」が表示されるようにしてみましょう。これを行うには、アプリケーションのrootパスをこのコントローラとアクションに対応付けます。

それではconfig/routes.rbを開き、Rails.application.routes.drawブロックを以下のように書き換えてみましょう。

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
end

ブラウザでhttp://localhost:3000を開くと、「Hello, Rails!」テキストが表示されるはずです。これで、rootルーティングがArticlesControllerindexアクションに対応付けられたことを確認できました。

ルーティングについて詳しくはRailsのルーティングを参照してください。

5 自動読み込み

Railsアプリケーションでは、アプリケーションコードを読み込むのにrequireを書く必要はありません

おそらくお気づきかもしれませんが、ArticlesControllerApplicationControllerを継承しているにもかかわらず、app/controllers/articles_controller.rbには以下のような記述がどこにもありません。

require "application_controller" # 実際には書いてはいけません

Railsでは、アプリケーションのクラスやモジュールはどこでも利用できるようになっているので、上のようにrequireを書く必要はありませんし、app/ディレクトリの下で何かを読み込むためにrequire書いてはいけません。この機能は「自動読み込み(autoloading: オートロード)」と呼ばれています。詳しくはガイドの『Railsの自動読み込みと再読み込み』を参照してください。

requireを書く必要があるのは、以下の2つの場合だけです。

  • lib/ディレクトリの下にあるファイルを読み込む場合
  • Gemfilerequire: falseが指定されているgem依存を読み込む場合

6 MVCを理解する

これまでに、「ルーティング」「コントローラ」「アクション」「ビュー」について解説しました。これらはMVC(Model-View-Controller)と呼ばれるパターンに沿ったWebアプリケーションの典型的な構成要素です。MVCは、アプリケーションの責務を分割して理解しやすくするデザインパターンです。Railsは、このデザインパターンに従う規約になっています。

コントローラと、それに対応して動作するビューを作成したので、次の構成要素である「モデル」を生成しましょう。

6.1 モデルを生成する

モデル(model)とは、データを表現するためのRubyクラスです。モデルは、Active Recordと呼ばれるRailsの機能を介して、アプリケーションのデータベースとやりとりできます。

モデルを定義するには、以下のようにモデル用のジェネレータを実行します。

$ bin/rails generate model Article title:string body:text

モデル名は常に英語の「単数形」で表記してください。理由は、インスタンス化されたモデルは1件のデータレコードを表すからです。この規約を覚えるために、モデルのコンストラクタを呼び出すときにArticle.new(...)と単数形で書くことはあっても、複数形のArticles.new(...)書かないことを考えてみるとよいでしょう。

ジェネレータを実行すると、以下のようにいくつものファイルが作成されます。

invoke  active_record
create    db/migrate/<タイムスタンプ>_create_articles.rb
create    app/models/article.rb
invoke    test_unit
create      test/models/article_test.rb
create      test/fixtures/articles.yml

生成されたファイルのうち、マイグレーションファイル(db/migrate/<タイムスタンプ>_create_articles.rb)とモデルファイル(app/models/article.rb)の2つを中心に解説します。

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

マイグレーション(migration)は、アプリケーションのデータベース構造を変更するときに使われる機能です。RailsアプリケーションのマイグレーションはRubyコードで記述するので、データベースの種類を問わずにマイグレーションを実行できます。

db/migrate/ディレクトリの下に生成されたマイグレーションファイルを開いてみましょう。

class CreateArticles < ActiveRecord::Migration[7.1]
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

create_tableメソッド呼び出しは、articlesテーブルの構成方法を指定します。create_tableメソッドは、デフォルトでidカラムを「オートインクリメントの主キー」として追加します。つまり、テーブルで最初のレコードのidは1、次のレコードのidは2、というように自動的に増加します。

create_tableのブロック内には、titlebodyという2つのカラムが定義されています。これらのカラムは、先ほど実行したbin/rails generate model Article title:string body:textコマンドで指定したので、ジェネレータによって自動的に追加されました。

ブロックの末尾行はt.timestampsメソッドを呼び出しています。これはcreated_atupdated_atという2つのカラムを追加で定義します。後述するように、これらのカラムはRailsによって自動で管理されるので、モデルオブジェクトを作成・更新すると、これらのカラムに値が自動で設定されます。

それでは以下のコマンドでマイグレーションを実行しましょう。

$ bin/rails db:migrate

マイグレーションコマンドを実行すると、データベース上にテーブルが作成されます。

==  CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0018s
==  CreateArticles: migrated (0.0018s) ==========================

マイグレーションについて詳しくは、Active Recordマイグレーションを参照してください。

これで、モデルを介してテーブルとやりとりできるようになりました。

6.3 モデルを用いてデータベースとやりとりする

Railsのコンソール機能を使って、モデルで少し遊んでみましょう。Railsコンソールは、Rubyのirbと同様の対話的コーディング環境ですが、irbと違うのは、Railsとアプリケーションコードも自動的に読み込まれる点です。

以下を実行してRailsコンソールを起動しましょう。

$ bin/rails console

以下のようなirbプロンプトが表示されるはずです。

Loading development environment (Rails 7.1.0)
irb(main):001:0>

このプロンプトで、先ほど作成したArticleオブジェクトを以下のように初期化できます。

irb> article = Article.new(title: "Hello Rails", body: "I am on Rails!")

ここで重要なのは、このオブジェクトは単に初期化されただけの状態であり、まだデータベースに保存されていないことです。つまり、このオブジェクトはこのコンソールでしか利用できません(コンソールを終了すると消えてしまいます)。オブジェクトをデータベースに保存するには、saveメソッドを呼び出さなくてはなりません。

irb> article.save
(0.1ms)  begin transaction
Article Create (0.4ms)  INSERT INTO "articles" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Hello Rails"], ["body", "I am on Rails!"], ["created_at", "2020-01-18 23:47:30.734416"], ["updated_at", "2020-01-18 23:47:30.734416"]]
(0.9ms)  commit transaction
=> true

上の出力には、INSERT INTO "Article" ...というデータベースクエリも表示されています。これは、その記事がテーブルにINSERT(挿入)されたことを示しています。そして、articleオブジェクトをもう一度表示すると、先ほどと何かが変わっていることがわかります。

irb> article
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

オブジェクトにidcreated_atupdated_atという属性(attribute)が設定されています。先ほどオブジェクトをsaveしたときにRailsが追加してくれたのです。

この記事をデータベースから取り出したければ、そのモデルでfindメソッドを呼び出し、その記事のidを引数として渡します。

irb> Article.find(1)
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

データベースに保存されているすべての記事を取り出すには、そのモデルでallメソッドを呼び出します。

irb> Article.all
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">]>

このメソッドが返すActiveRecord::Relationオブジェクトは、一種の超強力な配列と考えるとよいでしょう。

モデルについて詳しくは、Active Record の基礎Active Record クエリインターフェイスを参照してください。

モデルは、MVCというパズルの最後のピースです。次は、これらのピースをつなぎ合わせてみましょう。

6.4 記事のリストを表示する

app/controllers/articles_controller.rbコントローラを再度開いて、データベースからすべての記事を取り出せるようindexアクションを変更します。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
end

コントローラ内のインスタンス変数(@で始まる変数)は、ビューからも参照できます。つまり、app/views/articles/index.html.erb@articlesと書くと、このインスタンス変数を参照できるということです。このファイルを開いて、以下のように書き換えます。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= article.title %>
    </li>
  <% end %>
</ul>

上記のコードでは、HTMLの中にERBEmbedded Ruby)も書かれています。ERBとは、ドキュメントに埋め込まれたRubyコードを評価するテンプレートシステムのことです。 ここでは、<% %><%= %>という2種類のERBタグが使われています。<% %>タグは「この中のRubyコードを評価する」という意味です。<%= %>タグは「この中のRubyコードを評価し、返された値を出力する」という意味です。 これらのERBタグの中には、通常のRubyプログラムで書けるコードなら何でも書けますが、読みやすさのため、ERBタグにはなるべく短いコードを書く方がよいでしょう。

上のコードでは、@articles.eachが返す値は画面に出力したくないので<% %> で囲んでいますが、(各記事の)article.title が返す値は画面に出力したいので<%= %> で囲んでいます。

ブラウザでhttp://localhost:3000を開くと最終的な結果を確認できます(bin/rails serverを実行しておくことをお忘れなく)。このときの動作は以下のようになります。

  1. ブラウザはGET http://localhost:3000というリクエストをサーバーに送信する。
  2. Railsアプリケーションがこのリクエストを受信する。
  3. RailsルーターがrootルーティングをArticlesControllerindexアクションに割り当てる。
  4. indexアクションは、Articleモデルを用いてデータベースからすべての記事を取り出す。
  5. Railsが自動的にapp/views/articles/index.html.erbビューをレンダリングする。
  6. ビューにあるERBコードが評価されてHTMLを出力する。
  7. サーバーは、HTMLを含むレスポンスをブラウザに送信する。

これでMVCのピースがすべてつながり、コントローラに最初のアクションができました。このまま次のアクションを作ってみましょう。

7 CRUDの重要性

ほぼすべてのWebアプリケーションは、何らかの形でCRUD(Create、Read、Update、Delete)操作を行います。Webアプリケーションで行われる処理の大半はCRUDです。Railsフレームワークはこの点を認識しており、CRUDを行うコードをシンプルに書ける機能を多数備えています。

それでは、アプリケーションに機能を追加してこれらの機能を探ってみましょう。

7.1 記事を1件表示する

現在のビューは、データベースにある記事をすべて表示します。今度は、1件の記事のタイトルと本文を表示するビューを追加してみましょう。

手始めに、コントローラの新しいアクションに対応付けられる新しいルーティングを1個追加します(アクションはこの後で追加します)。config/routes.rbを開き、ルーティングの末尾に以下のように追加します。

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
  get "/articles/:id", to: "articles#show"
end

追加したルーティングもgetルーティングですが、パスの末尾に:idが追加されている点が異なります。これはルーティングのパラメータ(parameter)を指定します。ルーティングパラメータは、リクエストのパスに含まれる特定の値をキャプチャして、その値をparamsというハッシュに保存します。paramsはコントローラのアクションでもアクセスできます。たとえばGET http://localhost:3000/articles/1というリクエストを扱う場合、:id1がキャプチャされ、ArticlesControllershowアクションでparams[:id]と書くとアクセスできます。

それでは、showアクションをapp/controllers/articles_controller.rbindexアクションの下に追加しましょう。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end
end

このshowアクションでは、前述Article.findメソッドを呼び出すときに、ルーティングパラメータでキャプチャしたidを渡しています。返された記事は@articleインスタンス変数に保存されているので、ビューから参照できます。showアクションは、デフォルトでapp/views/articles/show.html.erbをレンダリングします。

今度はapp/views/articles/show.html.erbを作成し、以下のコードを書きます。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

これで、http://localhost:3000/articles/1を開くと記事が1件表示されるようになりました。

仕上げとして、記事ページを開くときによく使われる方法を追加しましょう。app/views/articles/index.html.erbにリスト表示される記事タイトルに、その記事へのリンクを追加します。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="/articles/<%= article.id %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

7.2 リソースフルルーティング

ここまでにCRUDのR(Read)をやってみました。最終的にCRUDのC(Create)、U(Update)、D(Delete)も行います。既にお気づきかと思いますが、CRUDを追加するときは「ルーティングを追加する」「コントローラにアクションを追加する」「ビューを追加する」という3つの作業を行います。「ルーティング」「コントローラのアクション」「ビュー」がどんな組み合わせになっても、エンティティに対するCRUD操作に落とし込まれます。こうしたエンティティはリソース(resource)と呼ばれます。たとえば、このアプリケーションの場合は「1件の記事」が1個のリソースに該当します。

Railsはresourcesというルーティングメソッドを提供しており、メソッド名が複数形であることからわかるように、リソースのコレクション(collection: 集まり)を対応付けるのによく使われるルーティングをすべて対応付けてくれます。C(Create)、U(Update)、D(Delete)に進む前に、config/routes.rbでこれまでgetメソッドで書かれていたルーティングをresourcesで書き換えましょう。

Rails.application.routes.draw do
  root "articles#index"

  resources :articles
end

ルーティングがどのように対応付けられているかを表示するには、bin/rails routesコマンドが使えます(訳注: Rails 7では以下のルーティングの下にTurboやAction MailboxやActive Storageなどのルーティングも表示されますが、ここでは無視して構いません)。

$ bin/rails routes
      Prefix Verb   URI Pattern                  Controller#Action
        root GET    /                            articles#index
    articles GET    /articles(.:format)          articles#index
 new_article GET    /articles/new(.:format)      articles#new
     article GET    /articles/:id(.:format)      articles#show
             POST   /articles(.:format)          articles#create
edit_article GET    /articles/:id/edit(.:format) articles#edit
             PATCH  /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy

resourcesメソッドは、_urlで終わる「URL」ヘルパーメソッドと、_pathで終わる「パス」ヘルパーメソッドも自動的に設定します。パスヘルパーを使うことで、コードが特定のルーティング設定に依存することを避けられます。Prefixカラムの値の末尾には、パスヘルパーによって_url_pathといったサフィックスが追加されます。たとえば、記事を1件渡されると、article_pathヘルパーは"/articles/#{article.id}"を返します。このパスヘルパーを用いると、app/views/articles/index.html.erbのリンクを簡潔な形に書き直せます。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="<%= article_path(article) %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

しかし、link_toヘルパーを用いると<a>タグが不要になるので、さらに便利です。link_toヘルパーの第1引数はリンクテキスト、第2引数はリンク先です。第2引数にモデルオブジェクトを渡すと、link_toが適切なパスヘルパーを呼び出してオブジェクトをパスに変換します。たとえば、link_toにarticleを渡すとarticle_pathというパスヘルパーが呼び出されます。これを用いると、app/views/articles/index.html.erbは以下のように書き換えられます。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

スッキリしましたね!

ルーティングについて詳しくはRailsのルーティングを参照してください。

7.3 記事を1件作成する

次はCRUDのC(Create)です。典型的なWebアプリケーションでは、リソースを1個作成するのに複数のステップを要します。最初にユーザーがフォーム画面をリクエストします。次にユーザーがそのフォームに入力して送信します。エラーが発生しなかった場合はリソースが作成され、リソース作成に成功したことを何らかの形で表示します。エラーが発生した場合はフォーム画面をエラーメッセージ付きで再表示し、フォーム送信の手順を繰り返します。

Railsアプリケーションでは、通常これらのステップを実現するときにnewアクションとcreateアクションを組み合わせて扱います。それでは2つのアクションをapp/controllers/articles_controller.rbshowアクションの下に典型的な実装として追加してみましょう。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(title: "...", body: "...")

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end
end

newアクションは、新しい記事を1件インスタンス化しますが、データベースには保存しません。インスタンス化された記事は、ビューでフォームをビルドするときに使われます。newアクションを実行すると、app/views/articles/new.html.erb(この後作成します)がレンダリングされます。

createアクションは、タイトルと本文を持つ新しい記事をインスタンス化し、データベースへの保存を試みます。記事の保存に成功すると、その記事のページ("http://localhost:3000/articles/#{@article.id}")にリダイレクトします。記事の保存に失敗した場合は、app/views/articles/new.html.erbに戻ってフォームを再表示し、Turboが正常に動作するようにステータスコード422 Unprocessable Entityを返します(unprocessable_entity)。なお、このときの記事タイトルと本文にはダミーの値が使われます。これらはフォームが作成された後でユーザーが変更することになります。

redirect_toメソッドを使うとブラウザで新しいリクエストが発生しますが、renderメソッドは指定のビューを現在のリクエストとしてレンダリングします。ここで重要なのは、redirect_toメソッドはデータベースやアプリケーションのステートが変更された「後で」呼び出すべきであるという点です。ステートが変更される前にredirect_toを呼び出すと、ユーザーがブラウザをリロードしたときに同じリクエストが再送信され、変更が重複してしまいます。

7.3.1 フォームビルダーを使う

ここではRailsのフォームビルダー(form builder)という機能を使います。フォームビルダーを使えば、最小限のコードを書くだけで設定がすべてできあがったフォームを表示でき、かつRailsの規約に沿うことができます。

それではapp/views/articles/new.html.erbを作成して以下のコードを書き込みましょう。

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </div>

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

form_withヘルパーメソッドは、フォームビルダー(ここではform)をインスタンス化します。form_withのブロック内でフォームビルダーのlabeltext_fieldといったメソッドを呼び出すと、適切なフォーム要素が出力されます。

form_withを呼び出したときの出力結果は以下のようになります。

<form action="/articles" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="...">

  <div>
    <label for="article_title">Title</label><br>
    <input type="text" name="article[title]" id="article_title">
  </div>

  <div>
    <label for="article_body">Body</label><br>
    <textarea name="article[body]" id="article_body"></textarea>
  </div>

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

フォームビルダーについて詳しくは、Action View フォームヘルパーを参照してください。

7.3.2 Strong Parametersを使う

送信されたフォームのデータはparamsハッシュに保存され、ルーティングパラメータも同様にキャプチャされます。つまりcreateアクションでは、params[:article][:title]を用いると送信された記事タイトルにアクセスでき、params[:article][:body]を用いると送信された記事本文にアクセスできます。こうした値を個別にArticle.newに渡すことも一応可能ですが、値の数が増えれば増えるほどコードが煩雑になり、コーディング中のミスも増えます。

そこで、さまざまな値を個別に渡すのではなく、それらの値を含む1個のハッシュを渡します。しかしその場合も、ハッシュ内でどのような値が許されているかを厳密に指定しなければなりません。これを怠ると、悪意のあるユーザーがブラウザ側でフィールドをこっそり追加して、機密データを上書きする可能性が生じるので危険です。ただし実際には、params[:article]をフィルタなしでArticle.newに直接渡すと、RailsがForbiddenAttributesErrorエラーを出してこの問題を警告するようになっています。そこで、RailsのStrong Parametersという機能を用いてparamsをフィルタすることにします。ここで言うstrongとは、params強く型付けする(strong typing)とお考えください。

それでは、app/controllers/articles_controller.rbの末尾に article_paramsというprivateメソッドを追加し、paramsをフィルタしましょう。さらに、createアクションでこのメソッドを使うように変更します。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

Strong Parametersについて詳しくは、Action Controller の概要 § Strong Parametersを参照してください。

7.3.3 バリデーションとエラーメッセージの表示

これまで見てきたように、リソースの作成は単独のステップではなく、複数のステップで構成されています。その中には、無効なユーザー入力を適切に処理することも含まれます。Railsには、無効なユーザー入力を処理するためにバリデーション(validation: 検証)という機能が用意されています。バリデーションとは、モデルオブジェクトを保存する前に自動的にチェックするルールのことです。チェックに失敗した場合は保存を中止し、モデルオブジェクトの errors 属性に適切なエラーメッセージが追加されます。

それでは、app/models/article.rbモデルにバリデーションをいくつか追加してみましょう。

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

1個目のバリデーションは、「titleの値が必ず存在しなければならない」ことを宣言しています。titleは文字列なので、titleにはホワイトスペース(スペース文字、改行、Tabなど)以外の文字が1個以上含まれていなければならないという意味になります。

2個目のバリデーションも、「bodyの値が必ず存在しなければならない」ことを宣言しています。さらに、bodyの値は10文字以上でなければならないことも宣言しています。

title属性やbody属性がどこで定義されているかが気になる方へ: Active Recordは、テーブルのあらゆるカラムごとにモデル属性を自動的に定義するので、モデルファイル内でこれらの属性を宣言する必要はありません。

バリデーションを追加したので、今度はapp/views/articles/new.html.erbを変更してtitlebodyのエラーメッセージが表示されるようにしましょう。

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% @article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %><br>
    <% @article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

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

full_messages_forメソッドは、指定の属性に対応するわかりやすいエラーメッセージを含む配列を1個返します。その属性でエラーが発生していない場合、配列は空になります。

以上の追加がバリデーションでどのように動くかを理解するために、コントローラのnewアクションとcreateアクションをもう一度見てみましょう。

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

http://localhost:3000/articles/newをブラウザで表示すると、GET /articles/newリクエストはnewアクションに対応付けられます。newアクションは@articleを保存しないので、この時点ではバリデーションは実行されず、エラーメッセージも表示されません。

このフォームを送信すると、POST /articlesリクエストはcreateアクションに対応付けられます。createアクションは@article保存しようとするので、バリデーションが実行されます。バリデーションのいずれかが失敗すると、@articleは保存されず、レンダリングされたapp/views/articles/new.html.erbにエラーメッセージが表示されます。

バリデーションについて詳しくは、Active Record バリデーションを参照してください。バリデーションのエラーメッセージについてはActive Record バリデーション § バリデーションエラーに対応するを参照してください。

7.3.4 仕上げ

これで、ブラウザで http://localhost:3000/articles/new を表示すると記事を1件作成できるようになりました。仕上げに、app/views/articles/index.html.erbページの末尾からこの作成ページへのリンクを追加しましょう。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

7.4 記事を更新する

ここまでで、CRUDのうちCとRを実現しました。今度はUの部分、つまり更新を実装してみましょう。リソースの更新は、ステップが複数あるという点でリソースの作成と非常に似ています。最初に、ユーザーはデータを編集するフォームをリクエストします。次に、ユーザーがフォームにデータを入力して送信します。エラーが発生しなければ、リソースは更新されます。エラーが発生した場合はフォームをエラーメッセージ付きで再表示し、同じことを繰り返します。

更新のステップは、コントローラのeditアクションとupdateアクションで扱うのが慣例です。それでは、app/controllers/articles_controller.rbcreateアクションの下にこれらのアクションの典型的な実装を追加してみましょう。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

更新に用いるeditアクションおよびupdateアクションが、作成に用いるnewアクションおよびcreateとほとんど同じである点にご注目ください。

editアクションはデータベースから記事を取得して@articleに保存し、フォームを作成するときに使えるようにします。editアクションは、デフォルトでapp/views/articles/edit.html.erbをレンダリングします。

updateアクションはデータベースから記事を(再)取得し、 article_paramsでフィルタリングされた送信済みのフォームデータで更新を試みます。 バリデーションが失敗せずに更新が成功した場合、ブラウザを更新後の記事ページにリダイレクトします。更新に失敗した場合はapp/views/articles/edit.html.erbをレンダリングし、同じフォームをエラーメッセージ付きで再表示します。

7.4.1 ビューのコードをパーシャルで共有する

editで使うフォームの表示は、newで使うフォームの表示と同じに見えます。さらに、Railsのフォームビルダーとリソースフルルーティングのおかげで、コードも同じになっています。フォームビルダーは、モデルオブジェクトが既に保存されている場合はedit用のフォームを、モデルオブジェクトが保存されていない場合はnew用のフォームを自動的に構成するので、状況に応じて適切なリクエストを行えます。

どちらのフォームにも同じコードが使われているので、パーシャル(partial: 部分テンプレートとも呼ばれます)と呼ばれる共有ビューにまとめることにします。以下の内容で app/views/articles/_form.html.erb を作成してみましょう。

<%= form_with model: article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %><br>
    <% article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

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

上記のコードはapp/views/articles/new.html.erbのフォームと同じですが、すべての@articlearticleに置き換えてある点にご注目ください。パーシャルのコードは共有されるので、特定のインスタンス変数に依存しないようにするのがベストプラクティスです(コントローラのアクションで設定されるインスタンス変数に依存すると、他で使い回すときに不都合が生じます)。代わりに、記事をローカル変数としてパーシャルに渡します。

render でパーシャルを使うために、app/views/articles/new.html.erbを以下の内容で置き換えてみましょう。

<h1>New Article</h1>

<%= render "form", article: @article %>

パーシャルのファイル名は冒頭にアンダースコア_必ず付けなければなりません(例: _form.html.erb)。ただし、レンダリングでパーシャルを参照するときはアンダースコアを付けません(例: render "form")。

続いて、同じ要領で以下の内容のapp/views/articles/edit.html.erbも作ってみましょう。

<h1>Edit Article</h1>

<%= render "form", article: @article %>

パーシャルについて詳しくは、レイアウトとレンダリング § パーシャルを使うを参照してください。

7.4.2 仕上げ

これで、記事のeditページ(http://localhost:3000/articles/1/editなど)にアクセスして記事を更新できるようになりました。最後に、app/views/articles/show.html.erb の末尾に以下のようなeditページへのリンクを追加してみましょう。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
</ul>

7.5 記事を削除する

いよいよCRUDのDまで到達しました。リソースの削除はリソースの作成や更新よりもシンプルなので、必要なのは削除用のルーティングとコントローラのアクションだけです。削除用のルーティングは、DELETE /articles/:idリクエストをArticlesControllerdestroyアクションに対応付けます。

それでは、app/controllers/articles_controller.rbupdateアクションの下に典型的なdestroyアクションを追加してみましょう。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

destroyアクションは、データベースから記事を取得してdestroyメソッドを呼び出しています。次にブラウザをステータスコード303 See Otherでrootパスにリダイレクトします。

rootパスにリダイレクトすることに決めたのは、そこが記事へのメインのアクセスポイントだからです。しかし状況によっては、たとえばarticles_pathにリダイレクトすることもあります。

それでは、app/views/articles/show.html.erb の下部に削除用ボタンを追加して、ページの記事を削除できるようにしましょう。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

上のコードでは、dataオプションを使って"Destroy"リンクのHTML属性data-turbo-methoddata-turbo-confirmを設定しています。どちらの属性も、新しいRailsアプリケーションにデフォルトで含まれているTurboにフックします。 data-turbo-method="delete"を指定すると、GETリクエストではなくDELETEリクエストが送信されます。 data-turbo-confirm="Are you sure?" を指定すると、リンクをクリックしたときに「Are you sure?」ダイアログが表示され、ユーザーが「キャンセル」をクリックするとリクエストを中止します。

以上でできあがりです!記事のリスト表示も、作成も、更新も思いのままです。CRUDバンザイ!

8 モデルを追加する

今度はアプリケーションに第2のモデルを追加しましょう。第2のモデルは記事へのコメントを扱います。

8.1 第2のモデルを追加する

先ほどArticleモデルの作成に使ったのと同じジェネレーターを見てみましょう。今回は、Articleモデルへの参照を保持するComment モデルを作成します。ターミナルで以下のコマンドを実行します。

$ bin/rails generate model Comment commenter:string body:text article:references

コマンドを実行すると、以下の4つのファイルが作成されます。

ファイル 目的
db/migrate/20140120201010_create_comments.rb データベースにコメント用のテーブルを作成するためのマイグレーションファイル(ファイル名のタイムスタンプはこれとは異なります)
app/models/comment.rb Commentモデル
test/models/comment_test.rb Commentモデルをテストするためのハーネス
test/fixtures/comments.yml テストで使うサンプルコメント

手始めにapp/models/comment.rbを開いてみましょう。

class Comment < ApplicationRecord
  belongs_to :article
end

Commentモデルの内容は、これまでに見たArticleモデルと非常によく似ています。違いは、Active Recordの関連付け(アソシエーション: association)を設定するbelongs_to :articleという行がある点です。関連付けについて詳しくは、本ガイドの次のセクションで解説します。

シェルコマンドで使われている:referencesキーワードは、モデルの特殊なデータ型を表し 、指定されたモデル名の後ろに_idを追加した名前を持つ新しいカラムをデータベーステーブルに作成します。マイグレーションの実行後にdb/schema.rbファイルを調べてみると理解しやすいでしょう。

モデルファイルの他に、以下のようなマイグレーションファイルも生成されています。マイグレーションファイルは、モデルに対応するデータベーステーブルを生成するのに使います。

class CreateComments < ActiveRecord::Migration[7.1]
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :article, null: false, foreign_key: true

      t.timestamps
    end
  end
end

t.referencesという行は、article_idという名前のinteger型カラムとそのインデックス、そしてarticlesidカラムを指す外部キー制約を設定します。それではマイグレーションを実行しましょう。

$ bin/rails db:migrate

Railsは、これまで実行されていないマイグレーションだけを適切に判定して実行するので、以下のようなメッセージだけが表示されるはずです。

==  CreateComments: migrating =================================================
-- create_table(:comments)
   -> 0.0115s
==  CreateComments: migrated (0.0119s) ========================================

8.2 モデル同士を関連付ける

Active Recordの関連付け機能により、2つのモデルの間にリレーションシップを簡単に宣言することができます。今回の記事とコメントというモデルの場合、以下のいずれかの方法で関連付けを設定できます。

  • 1件のコメントは1件の記事に属する(Each comment belongs to one article)。
  • 1件の記事はコメントを複数持てる(One article can have many comments)。

そして上の方法(における英語の記述)は、Railsで関連付けを宣言するときの文法と非常に似ています。Commentモデル(app/models/comment.rb)内のコードに既に書かれていたように、各コメントは1つの記事に属しています。

class Comment < ApplicationRecord
  belongs_to :article
end

今度は、Articleモデル(app/models/article.rb)を編集して、関連付けの他方のモデルをここに追加する必要があります。

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

2つのモデルで行われているこれらの宣言によって、さまざまな動作が自動化されます。たとえば、@articleというインスタンス変数に記事が1件含まれていれば、@article.commentsと書くだけでその記事に関連付けられているコメントをすべて取得できます。

Active Recordの関連付けについて詳しくは、Active Recordの関連付けガイドを参照してください。

8.3 コメントへのルーティングを追加する

articlesコントローラで行ったときと同様、commentsを参照するためにRailsが認識すべきルーティングを追加する必要があります。再びconfig/routes.rbファイルを開き、以下のように変更してください。

Rails.application.routes.draw do
  root "articles#index"

  resources :articles do
    resources :comments
  end
end

この設定により、articlesの内側にネストしたリソース(nested resource)としてcommentsが作成されます。これは、モデルの記述とは別の視点から、記事とコメントの間のリレーションシップを階層的に捉えたものです。

ルーティングについて詳しくはRailsのルーティングガイドを参照してください。

8.4 コントローラを生成する

モデルを手作りしたので、モデルに合うコントローラも作ってみたくなります。それでは、再びこれまでと同様にジェネレータを使ってみましょう。

$ bin/rails generate controller Comments

上のコマンドを実行すると、以下の3つのファイルと1つの空ディレクトリが作成されます。

ファイル/ディレクトリ 目的
app/controllers/comments_controller.rb コメント用コントローラ
app/views/comments/ このコントローラのビューはここに置かれる
test/controllers/comments_controller_test.rb このコントローラのテスト用ファイル
app/helpers/comments_helper.rb ビューヘルパー

一般的なブログと同様、このブログの記事を読んだ人はそこに直接コメントを追加したくなるでしょう。そしてコメントを追加後に元の記事表示ページに戻り、コメントがそこに反映されていることを確認したいはずです。そこで、CommentsControllerを用いてコメントを作成したり、スパムコメントが書き込まれたら削除できるようにしたいと思います。

そこで最初に、Articleのshowテンプレート(app/views/articles/show.html.erb)を改造して新規コメントを作成できるようにしましょう。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

上のコードでは、Articleのshowページにフォームが1つ追加されています。このフォームはCommentsControllercreateアクションを呼び出すことでコメントを新規作成します。form_with呼び出しでは配列を1つ渡しています。これは/articles/1/commentsのような「ネストしたルーティング(nested route)」を生成します。

今度はapp/controllers/comments_controller.rbcreateアクションを改造しましょう。

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  private
    def comment_params
      params.require(:comment).permit(:commenter, :body)
    end
end

上のコードは、Articleコントローラのコードを書いていたときよりも少しだけ複雑に見えます。これはネストを使ったことによるものです。コメント関連のリクエストでは、コメントがどの記事に追加されるのかを追えるようにしておく必要があります。そこで、Articleモデルのfindメソッドを最初に呼び出し、リクエストで言及されている記事(のオブジェクト)を取得して@articleに保存しています。

さらにこのコードでは、関連付けによって有効になったメソッドをいくつも利用しています。@article.commentsに対してcreateメソッドを実行することで、コメントの作成と保存を同時に行っています(訳注: buildメソッドにすれば作成のみで保存は行いません)。この方法でコメントを作成すると、コメントと記事が自動的にリンクされ、指定された記事に対してコメントが従属するようになります。

新しいコメントの作成が完了したら、article_path(@article)ヘルパーを用いて元の記事の画面に戻ります。既に説明したように、このヘルパーを呼び出すとArticlesControllershowアクションが呼び出され、show.html.erbテンプレートがレンダリングされます。この画面にコメントを表示したいので、app/views/articles/show.html.erbに以下のコードを追加しましょう。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

これで、ブログに記事やコメントを自由に追加して、それらを正しい場所に表示できるようになりました。

記事にコメントが追加された様子

9 リファクタリング

さて、ブログの記事とコメントが動作するようになったので、ここでapp/views/articles/show.html.erbテンプレートを見てみましょう。何やらコードがたくさん書かれていて読みにくくなっています。ここでもパーシャルを使ってコードをきれいにしましょう。

9.1 パーシャルコレクションをレンダリングする

最初に、特定記事のコメントをすべて表示する部分を切り出してコメントパーシャルを作成しましょう。app/views/comments/_comment.html.erbというファイルを作成し、以下のコードを入力します。

<p>
  <strong>Commenter:</strong>
  <%= comment.commenter %>
</p>

<p>
  <strong>Comment:</strong>
  <%= comment.body %>
</p>

続いて、app/views/articles/show.html.erbの内容を以下に置き換えます。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

これで、app/views/comments/_comment.html.erbパーシャルが、@article.commentsコレクションに含まれているコメントをすべてレンダリングするようになりました。renderメソッドが@article.commentsコレクションに含まれる要素を1つずつ列挙するときに、各コメントをパーシャルと同じ名前のローカル変数に自動的に代入します。この場合はcommentというローカル変数が使われるので、これをパーシャルの表示に利用できます。

9.2 パーシャルのフォームをレンダリングする

今度はコメント作成部分もパーシャルに追い出してみましょう。app/views/comments/_form.html.erbファイルを作成し、以下のように入力します。

<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

続いてapp/views/articles/show.html.erbの内容を以下で置き換えます。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= render 'comments/form' %>

2番目のrenderは、レンダリングするcomments/formパーシャルテンプレートを定義しているだけです。comments/formと書くだけで、Railsは区切りのスラッシュ文字を認識し、app/views/commentsディレクトリの_form.html.erbパーシャルをレンダリングすればよいということを理解し、実行してくれます。app/views/comments/_form.html.erbなどと書く必要はありません。

@articleオブジェクトはインスタンス変数なので、ビューでレンダリングされるどのパーシャルからもアクセスできます。

9.3 concernを使う

Railsの「concern(関心事)」とは、大規模なコントローラやモデルの理解や管理を楽にする手法の1つです。複数のモデル(またはコントローラ)が同じ関心を共有していれば、concernを介して再利用できるというメリットもあります。concernはRubyの「モジュール」で実装され、モデルやコントローラが担当する機能のうち明確に定義された部分を表すメソッドをそのモジュールに含めます。なおモジュールは他の言語では「ミックスイン」と呼ばれることもよくあります。

concernは、コントローラやモデルで普通のモジュールと同じように使えます。rails new blog でアプリを作成すると、app/内に以下の2つのconcernsフォルダも作成されます。

app/controllers/concerns
app/models/concerns

以下の例ではブログの新機能を実装しますが、この機能にはconcernを利用するのが有益です。そこで、この機能を利用するためにconcernを作成し、コードをリファクタリングすることで、コードをよりDRYでメンテナンスしやすくします。

1件のブログ記事はさまざまなステータスを持つ可能性があります。たとえば記事の可視性について「誰でも見てよい(public)」「著者だけに見せる(private)」というステータスを持つかもしれませんし、「復旧可能な形で記事を非表示にする(archived)」ことも考えられます。コメントについても同様に可視性やアーカイブを設定することもあるでしょう。こうしたステータスを表す方法の1つとして、モデルごとにstatusカラムを持たせるとしましょう。

以下のマイグレーション生成コマンドを実行してstatusカラムを追加した後で、ArticleモデルとCommentsモデルにstatusカラムを追加します。

$ bin/rails generate migration AddStatusToArticles status:string
$ bin/rails generate migration AddStatusToComments status:string

続いて以下を実行し、生成されたマイグレーションでデータベースを更新します。

$ bin/rails db:migrate

既存の記事やコメントのステータスを一括指定するには、生成されたマイグレーションファイルにdefault: "public"オプションを追加してデフォルト値を追加し、マイグレーションを再度実行する方法が使えます。あるいは、railsコンソールでArticle.update_all(status: "public")Comment.update_all(status: "public")を呼び出すことでも可能です。

マイグレーションについて詳しくは、Active Record マイグレーションガイドを参照してください。

次に、app/controllers/articles_controller.rbのStrong Parametersを以下のように更新して:statusキーも許可しておく必要があります。


  private
    def article_params
      params.require(:article).permit(:title, :body, :status)
    end

app/controllers/comments_controller.rbでも同様に:statusキーを許可します。


  private
    def comment_params
      params.require(:comment).permit(:commenter, :body, :status)
    end

bin/rails db:migrateマイグレーションを実行してstatusカラムを追加したら、Articleモデルを以下で置き換えます。

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }

  VALID_STATUSES = ['public', 'private', 'archived']

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == 'archived'
  end
end

Commentモデルも以下で置き換えます。

class Comment < ApplicationRecord
  belongs_to :article

  VALID_STATUSES = ['public', 'private', 'archived']

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == 'archived'
  end
end

次にindexアクションに対応するapp/views/articles/index.html.erbテンプレートで以下のようにarchived?メソッドを追加し、アーカイブ済みの記事を表示しないようにします。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

同様に、コメントのパーシャルビュー(app/views/comments/_comment.html.erb)にも、アーカイブ済みのコメントが表示されないようarchived?メソッドを書きます。

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

しかし、2つのモデルのコードを見返してみると、ロジックが重複していることがわかります。このままでは、今後ブログにプライベートメッセージ機能などを追加するとロジックがまた重複してしまうでしょう。concernは、このような重複を避けるのに便利です。

1つのconcernは、モデルの責務の「一部」についてのみ責任を負います。この例の場合、「関心(concern)」の対象となるメソッドはすべてモデルの可視性に関連しているので、新しいconcern(すなわちモジュール)をVisibleと呼ぶことにしましょう。app/models/concernsディレクトリの下にvisible.rbという新しいファイルを作成し、複数のモデルで重複していたステータス関連のすべてのメソッドをそこに移動します。

app/models/concerns/visible.rb

module Visible
  def archived?
    status == 'archived'
  end
end

ステータスを検証するメソッドもconcernにまとめられますが、バリデーションメソッドはクラスレベルで呼び出されるので、より複雑になります。APIドキュメントのActiveSupport::Concernには、以下のようにバリデーションをシンプルにincludedする方法が紹介されています。

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = ['public', 'private', 'archived']

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  def archived?
    status == 'archived'
  end
end

これで各モデルで重複しているロジックを取り除けるようになったので、新たにVisibleモジュールをincludeしましょう。

app/models/article.rbを以下のように変更します。

class Article < ApplicationRecord
  include Visible

  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

app/models/comment.rbも以下のように変更します。

class Comment < ApplicationRecord
  include Visible

  belongs_to :article
end

concernにはクラスメソッドも追加できます。たとえば、ステータスがpublicの記事(またはコメント)の件数をメインページに表示したい場合は、Visibleモジュールに以下の要領でクラスメソッドを追加します。

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = ['public', 'private', 'archived']

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  class_methods do
    def public_count
      where(status: 'public').count
    end
  end

  def archived?
    status == 'archived'
  end
end

これで、以下のようにindexビューで任意のクラスメソッドを呼べるようになります。

<h1>Articles</h1>

Our blog has <%= Article.public_count %> articles and counting!

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

仕上げとして、フォームにセレクトボックスを追加して、ユーザーが記事を作成したりコメントを投稿したりするときにステータスを選択できるようにします。

オブジェクトのステータスを選択する(ステータスが未設定の場合はpublicをデフォルトに指定する)ことも可能です。app/views/articles/_form.html.erbに以下を追加します。

<div>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: article.status || 'public' %>
</div>

app/views/comments/_form.html.erbにも以下を追加します。

<p>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: 'public' %>
</p>

10 コメントを削除する

スパムコメントを削除できるようにするのも、このブログでは重要な機能です。そのためのビューを作成し、CommentsControllerdestroyアクションを作成する必要があります。

最初にapp/views/comments/_comment.html.erbパーシャルに削除用のボタンを追加しましょう。

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <p>
    <%= link_to "Destroy Comment", [comment.article, comment], data: {
                  turbo_method: :delete,
                  turbo_confirm: "Are you sure?"
                } %>
  </p>
<% end %>

この新しい「Destroy Comment」リンクをクリックすると、DELETE /articles/:article_id/comments/:idというリクエストがCommentsControllerに送信されます。コントローラはそれを受け取って、どのコメントを削除すべきかを検索することになります。それではコントローラ(app/controllers/comments_controller.rb)にdestroyアクションを追加しましょう。

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  def destroy
    @article = Article.find(params[:article_id])
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    redirect_to article_path(@article), status: :see_other
  end

  private
    def comment_params
      params.require(:comment).permit(:commenter, :body, :status)
    end
end

このdestroyアクションは、まずどの記事が対象であるかを検索して@articleに保存し、続いて@article.commentsコレクションの中のどのコメントが対象であるかを特定して@commentに保存します。そしてそのコメントをデータベースから削除し、終わったら記事のshowアクションに戻ります。

10.1 関連付けられたオブジェクトも削除する

ある記事を削除したら、その記事に関連付けられているコメントも一緒に削除する必要があります(そうしないと、コメントがいつまでもデータベース上に残ってしまいます)。Railsでは関連付けにdependentオプションを指定することでこれを実現しています。Articleモデルapp/models/article.rbを以下のように変更しましょう。

class Article < ApplicationRecord
  include Visible

  has_many :comments, dependent: :destroy

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

11 セキュリティ

11.1 BASIC認証

このブログアプリケーションをオンラインで公開すると、このままでは誰でも記事を追加/編集/削除したり、コメントを削除したりできてしまいます。

Railsではこのような場合に便利な、非常にシンプルなHTTP認証システムが用意されています。

ArticlesControllerでは、認証されていない人物がアクションにアクセスできないようにブロックする必要があります。そこで、Railsのhttp_basic_authenticate_withメソッドを使うことで、このメソッドが許可する場合に限って、リクエストされたアクションにアクセスできるようにすることができます。

この認証システムを使うには、ArticlesControllerコントローラの冒頭部分で指定します。今回は、indexアクションとshowアクションは自由にアクセスできるようにし、それ以外のアクションには認証を要求するようにしたいと思います。そこで、app/controllers/articles_controller.rbに次の記述を追加します。

class ArticlesController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]

  def index
    @articles = Article.all
  end

  #(以下省略)
end

コメントの削除も認証済みユーザーにだけ許可したいので、CommentsControllerapp/controllers/comments_controller.rb)に以下のように追記します。

class CommentsController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy

  def create
    @article = Article.find(params[:article_id])
    # ...
  end

  #(以下省略)
end

これで、記事を新規作成しようとすると、以下のようなBASIC http認証ダイアログが表示されます。

Basic HTTP Authentication Challenge

正しいユーザー名とパスワードを入力すると、別のユーザー名とパスワードが要求されるか、ブラウザが閉じられるまで、認証された状態が続きます。

もちろん、Railsでは他の認証方法も使えます。Railsにはさまざまな認証システムがありますが、その中で人気が高い認証システムはDeviseAuthlogic gemの2つです。

11.2 その他のセキュリティ対策

セキュリティ、それもWebアプリケーションのセキュリティは非常に幅広く、かつ詳細に渡っています。Railsアプリケーションのセキュリティについて詳しくは、本ガイドのRailsセキュリティガイドを参照してください。

12 次に学ぶべきこと

以上で、Railsアプリケーションを初めて作るという試みは終わりです。この後は自由に更新したり実験を重ねたりできます。

もちろん、何の助けもなしにWebアプリケーションを作らなければならないなどということはないということを忘れてはなりません。RailsでWebアプリを立ち上げたり実行したりするうえで助けが必要になったら、以下のサポート用リソースを自由に参照できます。

13 設定の落とし穴

Railsでの無用なトラブルを避けるための最も初歩的なコツは、外部データを常にUTF-8エンコーディングで保存しておくことです。そうしておかないと、RubyライブラリやRailsがネイティブデータをたびたびUTF-8に変換しなければならず、しかも場合によっては失敗します。外部データのエンコーディングは常にUTF-8で統一することをおすすめします。

外部データのエンコードが統一されていないと、たとえば画面に黒い菱型や疑問符?が表示されたり、"ü"という文字のはずが"ü"という文字に化けたりするといった症状がよく発生します。Railsではこうした問題を緩和するため、問題の原因を自動的に検出して修正するために内部で多くの手順を行っています。しかし、UTF-8で保存されていない外部データがあると、Railsによる自動検出・修正が効かないデータで文字化けが発生することがあります。

UTF-8でないデータの主な原因は以下の2つです。

  • テキストエディタ: TextMateを含む多くのテキストエディタは、デフォルトでUTF-8エンコードでテキストを保存します。使っているテキストエディタがこのようになっていない場合、テンプレートを表示する時にéなどの特殊文字が◆?のようにブラウザ表示が文字化けすることがあります。これはi18n(国際化)用の翻訳ファイルで発生することもあります。DreamweaverのようにUTF-8保存がデフォルトでないエディタであっても、デフォルトをUTF-8に変更する方法は用意されているはずです。エンコードをUTF-8に変更してください。
  • データベース: Railsはデータベースから読みだしたデータを境界上でUTF-8に変換します。しかし、使っているデータベースの内部エンコード設定がUTF-8になっていない場合、UTF-8文字の一部をデータベースにそのまま保存できないことがあります。たとえばデータベースの内部エンコードがLatin-1になっていると、ロシア語・ヘブライ語・日本語などの文字をデータベースに保存したときにこれらの情報は永久に失われてしまいます。できるかぎり、データベースの内部エンコードはUTF-8にしておいてください。

14 参考資料(日本語)

フィードバックについて

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

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

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

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

支援・協賛

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

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