Rails をはじめよう

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

このガイドの内容:

  • Railsのインストール方法、新しいRailsアプリケーションの作成方法、アプリケーションからデータベースへの接続方法
  • Railsアプリケーションの一般的なレイアウト
  • MVC(モデル・ビュー・コントローラ)およびRESTfulデザインの基礎
  • Railsアプリケーションの原型を素早く立ち上げる方法

📜 お知らせ: Railsガイドが Carbon Ads for Open Source の対象になりました

1 本ガイドの前提条件

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

Railsとは、Rubyプログラミング言語の上で動作するWebアプリケーションフレームワークです。 Rubyの経験がまったくない場合、Railsを学ぶのはかなり大変な作業になるでしょう。Rubyを学ぶための精選されたオンラインリソース一覧はたくさんあるので、その中から以下をご紹介します。

いずれもよくできていますが、中には古いものもありますのでご注意ください。また、Railsでの日常的な開発に使う新しい文法が含まれていないこともあります。

2 Railsとは何か

Railsとは、Rubyプログラミング言語で書かれたWebアプリケーションフレームワークです。 Railsは、あらゆる開発者がWebアプリケーションの開発を始めるうえで必要となる作業やリソースを事前に仮定して準備しておくことで、Webアプリケーションをより簡単にプログラミングできるように設計されています。他の多くの言語によるWebアプリケーションフレームワークと比較して、アプリケーションを作成する際のコード量がより少なくて済むにもかかわらず、より多くの機能を実現できます。 Rails経験の長い多くの開発者から、おかげでWebアプリケーションの開発がとても楽しくなったという意見をいただいています。

Railsは、最善の開発方法というものを1つに定めるという、ある意味大胆な判断に基いて設計されています。Railsは、何かをなすうえで最善の方法というものが1つだけあると仮定し、それに沿った開発を全面的に支援します。言い換えれば、ここで仮定されている理想の開発手法に沿わない別の開発手法は行いにくくなるようにしています。この「The Rails Way」、「Rails流」とでもいうべき手法を学んだ人は、開発の生産性が著しく向上することに気付くでしょう。従って、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
  • Node.js
  • Yarn
3.1.1 Rubyをインストールする

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

$ ruby -v
ruby 2.5.0

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

Windowsユーザーは、Railsインストーラを用いてRuby on Railsを短時間でインストールできます。さまざまなOS環境でのインストール方法について詳しくは、ruby-lang.orgを参照してください。

Windowsで作業する場合は、Ruby Installer Development Kitもインストールすべきです。

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

3.1.2 SQLite3をインストールする

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

$ sqlite3 --version

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

3.1.3 Node.jsとYarnをインストールする

最後に、アプリケーションのJavaScriptを管理するNode.jsとYarnのインストールが必要です。

Node.jsのWebサイトのインストール方法に沿ってNode.jsをインストールします。次に以下のコマンドを実行して、正しくインストールできたかどうかを確認します。

$ node --version

Node.jsのバージョン番号が出力されるはずです。バージョンが8.16.0より大きいことを確認してください。

Yarnをインストールするには、YarnのWebサイトのインストール方法に沿って進めます。

インストール後、以下のコマンドを実行するとYarnのバージョン番号が出力されるはずです。

$ yarn --version

"1.22.0"のようなバージョン番号が表示されれば、Yarnは正しくインストールされています。

3.1.4 Railsをインストールする

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

$ gem install rails

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

$ rails --version

"Rails 6.0.0"などと表示されれば、次に進むことができます。

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

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

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

$ rails new blog

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

WSL(Windows Subsystem for Linux)を使っている場合、現時点ではファイルシステムの通知の一部に制限が生じます。つまり、rails new blog --skip-spring --skip-listenを実行してspring gemやlisten gemを無効にする必要があります。

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

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

$ cd blog

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

app/

  • ここにはアプリケーションのコントローラ、モデル、ビュー、ヘルパー、メイラー、チャンネル、ジョブズ、そしてアセットが置かれます。以後、本ガイドでは基本的にこのディレクトリを中心に説明を行います。

bin/

  • ここにはアプリケーションを起動/アップデート/デプロイするためのRailsスクリプトなどのスクリプトファイルが置かれます。

config/

config.ru

  • アプリケーションの起動に必要となる、Rackベースのサーバー用のRack設定ファイルです。Rackについて詳しくは、RackのWebサイトを参照してください。

db/

  • 現時点のデータベーススキーマと、データベースマイグレーションファイルが置かれます。

GemfileGemfile.lock

  • これらのファイルは、Railsアプリケーションで必要となるgemの依存関係を記述します。この2つのファイルはBundler gemで使われます。Bundlerについて詳しくはBundlerのWebサイトを参照してください。

lib/

  • アプリケーションで使う拡張モジュールが置かれます。

log/

  • アプリケーションのログファイルが置かれます。

package.json

  • Railsアプリケーションで必要なnpm依存関係をこのファイルで指定できます。このファイルはYarnで使われます。Yarnについて詳しくは、YarnのWebサイトを参照してください。

public/

  • このフォルダの下にあるファイルは外部(インターネット)からそのまま参照できます。静的なファイルやコンパイル済みアセットはここに置きます。

Rakefile

  • このファイルには、コマンドラインから実行できるタスクを記述します。ここでのタスク定義は、Rails全体のコンポーネントに対して定義されます。独自のRakeタスクを定義したい場合は、Rakefileに直接書くと権限が強すぎるので、なるべくlib/tasksフォルダの下にRake用のファイルを追加するようにしてください。

README.md

  • アプリケーションの概要を説明するマニュアルをここに記入します。このファイルにはアプリケーションの設定方法などを記入し、これさえ読めば誰でもアプリケーションを構築できるようにしておく必要があります。

storage/

  • Diskサービスで用いるActive Storageファイルが置かれます。詳しくはActive Storageの概要を参照してください。

test/

tmp/

  • キャッシュ、pidなどの一時ファイルが置かれます。

vendor/

  • サードパーティによって書かれたコードはすべてここに置きます。通常のRailsアプリケーションの場合、外部からのgemファイルをここに置きます。

.gitignore

  • Gitに登録しないファイル(またはパターン)をこのファイルで指定します。ファイルを登録しない方法について詳しくはGitHub - Ignoring filesを参照してください。

.ruby-version

  • デフォルトのRubyバージョンがこのファイルで指定されます。

4 Hello, Rails!

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

4.1 Webサーバーを起動する

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

$ rails server

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

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

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

Welcome画面のスクリーンショット

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

Railsの初期画面である「Welcome aboard」ページは、新しい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"

  # routes.rbで利用できるDSLについて詳しくはhttp://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
invoke  assets
invoke    scss
create      app/assets/stylesheets/articles.scss

この中で最も重要なのは、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/welcome/index.html.erbを開き、中身を以下に置き換えましょう。

<h1>Hello, Rails!</h1>

コントローラ用のジェネレータを実行するためにWebサーバーを停止していた場合は、bin/rails serverで再実行します。 http://localhost:3000/articles にアクセスするとテキストが表示されます。

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

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

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

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

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

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

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

5 MVCを理解する

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

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

5.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つを中心に解説します。

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

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

生成されたマイグレーションファイルを開いてみましょう。

class CreateArticles < ActiveRecord::Migration[6.0]
  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マイグレーションを参照してください。

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

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

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

$ bin/rails console

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

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

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

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というパズルの最後のピースです。次は、これらのピースをつなぎ合わせてみましょう。

5.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の中にERBも書かれています。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のピースがすべてつながり、コントローラに最初のアクションができました。このまま次のアクションに進みます。

6 CRUDの重要性

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

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

6.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というリクエストを扱う場合、:idの部分として1がキャプチャされ、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>

6.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コマンドが使えます。

$ 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」と「パスヘルパーメソッド」も設定します。パスヘルパーを使うことで、コードが特定のルーティング設定に依存することを避けられます。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ヘルパーを用いるとさらに便利になります。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のルーティングを参照してください。

6.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
    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に戻ってフォームを再表示します。なお、このときの記事タイトルと本文にはダミーの値が使われます。これらはフォームが作成された後でユーザーが変更することになります。

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

6.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 フォームヘルパーを参照してください。

6.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
    end
  end

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

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

6.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
    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 バリデーション § バリデーションエラーに対応するを参照してください。

6.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 %>

6.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
    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
    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をレンダリングし、同じフォームをエラーメッセージ付きで再表示します。

6.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に置き換えられている点にご注目ください。パーシャルのコードは共有されるので、特定のインスタンス変数に依存しないことがベストプラクティスです。コントローラのアクションで設定されるインスタンス変数に依存すると、他で使い回すときに不都合が生じます。そのため、記事をローカル変数としてパーシャルに渡しているのです。

app/views/articles/new.html.erb を更新して、render でパーシャルを使ってみましょう。

<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 %>

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

6.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>

6.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
    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
    end
  end

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

    redirect_to root_path
  end

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

destroyアクションは、データベースから記事を取得してdestroyメソッドを呼び出しています。続いてブラウザを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),
                  method: :delete,
                  data: { confirm: "Are you sure?" } %></li>
</ul>

上のコードでは、link_to にいくつか追加オプションを渡しています。method: :deleteオプションを指定すると、リンクはGETリクエストではなくDELETEリクエストを送信します。data: { confirm: "Are you sure?" }オプションを指定すると、リンクをクリックしたときに「本当に削除して良いですか?」と確認するダイアログが表示されます。ユーザーがこのダイアログをキャンセルすると、リクエストは中止されます。これらのオプションはいずれも、Unobtrusive JavaScript(UJS: 控えめなJavaScript)と呼ばれるRailsの機能を利用しています。これらの振る舞いを実装したJavaScriptファイルは、最近のRailsアプリケーションにデフォルトで含まれています。

控えめなJavaScriptについて詳しくは、Rails で JavaScript を使用するを参照してください。

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

7 モデルを追加する

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

7.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という行がある点です。関連付けについて詳しくは、本ガイドの次のセクションで説明します。

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

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

class CreateComments < ActiveRecord::Migration[6.0]
  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) ========================================

7.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の関連付けガイドを参照してください。

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

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

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

  resources :articles do
    resources :comments
  end
end

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

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

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

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

$ rails generate controller Comments

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

ファイル/ディレクトリ 目的
app/controllers/comments_controller.rb コメント用コントローラ
app/views/comments/ コントローラのビューはここに置かれる
test/controllers/comments_controller_test.rb コントローラのテスト用ファイル
app/helpers/comments_helper.rb ビューヘルパー
app/assets/stylesheets/comment.scss コントローラ用のCSS(カスケーディングスタイルシート)ファイル

一般的なブログと同様、このブログの記事を読んだ人はそこに直接コメントを追加したくなるでしょう。そしてコメントを追加後に元の記事表示ページに戻り、コメントがそこに反映されていることを確認したいはずです。そこで、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),
                  method: :delete,
                  data: { 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),
                  method: :delete,
                  data: { 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 %>

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

記事にコメントが追加されたところ

8 リファクタリング

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

8.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),
                  method: :delete,
                  data: { confirm: "Are you sure?" } %></li>
</ul>

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

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

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

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

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

<%= form_with(model: [ @article, @article.comments.build ], local: true) 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の内容を以下のように変更しましょう。

<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>

<p>
  <strong>Body:</strong>
  <%= @article.body %>
</p>

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

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

<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>

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

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

8.3 concernを使う

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

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

app/controllers/concerns
app/models/concerns

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

マイグレーションを実行して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 %>

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

1つの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には、以下のようにバリデーションをシンプルにincludeする方法が紹介されています。

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

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

<h1>Articles</h1>

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

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

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

アプリケーションに追加したstatusカラムを使うには、もう少し手を加える必要があります。最初に、以下のマイグレーションを実行してArticlesモデルとCommentsモデルにstatusカラムを追加します。

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

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

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

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

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

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

最後に、フォームにセレクトボックスを追加して、ユーザーが記事を作成したりコメントを投稿したりするときにステータスを選択できるようにします。デフォルトのステータスをpublicと指定することもできます。app/views/articles/_form.html.erbに以下を追加します。

<div>
  <%= form.label :status %><br>
  <%= form.select :status, ['public', 'private', 'archived'], selected: 'public' %>
</div>

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

<p>
  <%= form.label :status %><br>
  <%= form.select :status, ['public', 'private', 'archived'], selected: 'public' %>
</p>

9 コメントを削除する

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

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

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

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

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

この新しい「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)
  end

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

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

9.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

10 セキュリティ

10.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

  #(以下省略)

コメントの削除も認証済みユーザーにだけ許可したいので、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

  #(以下省略)

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

Basic HTTP Authentication Challenge

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

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

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

11 次に学ぶべきこと

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

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

12 設定の落とし穴

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にしておいてください。

13 参考資料

フィードバックについて

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

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

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

YassLab 株式会社
https://yasslab.jp/

支援・協賛

Railsガイドは下記のサポーターから継続的な支援を受けています。Railsガイドへの支援・協賛にご興味あれば info@yasslab.jp までお問い合わせください。

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