このガイドでは、Ruby on Rails(以下 Rails)を初めて設定して実行するまでを解説します。
このガイドの内容:
本ガイドは、Railsアプリケーションを構築したいと考えているRails初心者を対象にしています。読者にRailsの経験があることは前提としていません。
Railsとは、プログラミング言語「Ruby」の上で動作するWebアプリケーションフレームワークです。ただしプログラミング経験がまったくない人がいきなりRailsを学ぼうとすると、かなり大変な作業になるでしょう。オンラインで学べる洗練されたコンテンツはたくさんあるので、その中から以下をご紹介します。
いずれもよくできていますが中には古いものもあり、たとえば通常のRails開発で見かけるような最新の構文がカバーされていない可能性もあります。
訳注:Railsガイドでは開発経験者が早くキャッチアップできるよう、多くの用語説明を省略しています。読んでいて「難しい」と感じた場合はRailsチュートリアルからお読みください。
Railsとは、プログラミング言語「Ruby」で書かれたWebアプリケーションフレームワークです。Railsは、あらゆる開発者がWebアプリケーション開発で必要となる作業やリソースを事前に想定することで、Webアプリケーションをより手軽に開発できるように設計されています。他の多くのWebアプリケーションフレームワークと比較して、アプリケーションを開発する際のコード量がより少なくて済むにもかかわらず、より多くの機能を実現できます。ベテラン開発者の多くが「RailsのおかげでWebアプリケーション開発がとても楽しくなった」と述べています。
Railsは「最善の開発方法は1つである」という、ある意味大胆な判断に基いて設計されています。何かを行うための最善の方法を1つ仮定して、それに沿った開発を全面的に支援します。言い換えれば、Railsで仮定されていない別の開発手法は行いにくくなります。この「Rails Way」、すなわち「Railsというレールに乗って開発する」手法を学んだ人は、開発の生産性が驚くほど向上することに気付くでしょう。逆に、レールに乗らずに従来の開発手法にこだわると、開発の楽しさが減ってしまうかもしれません。
Railsの哲学には、以下の2つの主要な基本理念があります。
事前設定済みのDev Container開発環境を含む新規Railsアプリを作成できます。Dev Containerは、Railsを始める最短の方法です。手順については、Dev Containerでの開発ガイド を参照してください。
本ガイドを最大限に活用するには、以下の手順を1つずつすべて実行するのがベストです。どの手順もサンプルアプリケーションを動かすのに必要なものであり、それ以外のコードや手順は不要です。
本ガイドの手順に沿って作業すれば、blog
という名前の非常にシンプルなブログのRailsプロジェクトを作成できます。Railsアプリケーションを構築する前に、Rails本体をインストールしておいてください。
以下の例では、Unix系OSのプロンプトとして$
記号が使われていますが、プロンプトはカスタマイズ可能なので環境によって異なることもあります。Windowsではc:\source_code>
のように表示されます。
Railsをインストールする前に、必要な要件が自分のシステムで満たされているかどうかをチェックしましょう。少なくとも以下のソフトウェアが必要です。
訳注:GitHubが提供するクラウド開発環境『Codespaces』には、公式のRuby on Railsテンプレートが用意されています。Use this template
ボタンから、ワンクリックでRailsを動かせるクラウド開発環境が手に入ります。(参考: GitHub Codespaces を利用する - Rails Girls)
ターミナル(コマンドプロンプトとも言います)ウィンドウを開いてください。macOSの場合、ターミナル(Terminal.app)という名前のアプリケーションを実行します。Windowsの場合は[スタート]メニューから[ファイル名を指定して実行]をクリックして'cmd.exe'と入力します。$
で始まる記述はコマンド行なので、これらをコマンドラインに入力して実行します。次に以下を実行して、現在インストールされているRubyが最新バージョンであることを確認しましょう。
$ ruby --version ruby 3.1.0
RailsではRubyバージョン3.1.0以降が必須です。これより低いバージョン(2.3.7や1.8.7など)が表示された場合は、新たにRubyをインストールする必要があります。
RailsをWindowsにインストールする場合は、最初にRuby Installerをインストールしておく必要があります。
OS環境ごとのインストール方法について詳しくは、ruby-lang.orgを参照してください。
SQLite3データベースのインストールも必要です。 多くのUnix系OSには実用的なバージョンのSQLite3が同梱されています。Windowsの場合は、上述のRails InstalerでRailsをインストールするとSQLite3もインストールされます。その他の環境についてはSQLite3のインストール方法を参照してください。
$ sqlite3 --version
上を実行することでSQLite3のバージョンを確認できます。
Railsをインストールするには、gem install
コマンドを実行します。このコマンドはRubyGemsによって提供されます。
$ gem install rails
以下のコマンドを実行することで、すべて正常にインストールできたかどうかを確認できます。
$ rails --version Rails 7.2.0
"Rails 7.2.0"などのバージョンが表示されたら、次に進みましょう。
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ドキュメントを参照してください。 |
.github/ | GitHub固有のファイルが置かれます。 |
.gitignore | Gitに登録しないファイル(またはパターン)をこのファイルで指定します。Gitにファイルを登録しない方法について詳しくはGitHub - Ignoring filesを参照してください。 |
.rubocop.yml | このファイルにはRuboCop用の設定が含まれます。 |
.ruby-version | このファイルには、デフォルトのRubyバージョンが記述されています。 |
手始めに、画面に何かテキストを表示してみましょう。そのためには、Railsアプリケーションサーバーを起動しなくてはなりません。
先ほど作成した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のデフォルト情報ページが表示されます。
Webサーバーを停止するには、実行されているターミナルのウィンドウでCtrl + Cキーを押します。なお、development環境ではファイルに変更を加えればサーバーが自動的に変更を反映するので、サーバーの再起動は通常は不要です。
Railsの起動ページは、新しいRailsアプリケーションの「スモークテスト」として使えます。このページが表示されれば、サーバーが正常に動作していることが確認できます。
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
リクエストをArticlesController
のindex
アクションに対応付けます。
ArticlesController
とindex
アクションを作成するには、コントローラ用のジェネレータを実行します(上で既に適切なルーティングを追加したので、ここでは--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!」というテキストが表示されます。
この時点ではトップページ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
ルーティングがArticlesController
のindex
アクションに対応付けられたことを確認できました。
ルーティングについて詳しくはRailsのルーティングを参照してください。
Railsアプリケーションでは、アプリケーションコードを読み込むのにrequire
を書く必要はありません。
おそらくお気づきかもしれませんが、ArticlesController
はApplicationController
を継承しているにもかかわらず、app/controllers/articles_controller.rb
には以下のような記述がどこにもありません。
require "application_controller" # 実際には書いてはいけません
Railsでは、アプリケーションのクラスやモジュールはどこでも利用できるようになっているので、上のようにrequire
を書く必要はありませんし、app/
ディレクトリの下で何かを読み込むためにrequire
を書いてはいけません。この機能は「自動読み込み(autoloading: オートロード)」と呼ばれています。詳しくはガイドの『Railsの自動読み込みと再読み込み』を参照してください。
require
を書く必要があるのは、以下の2つの場合だけです。
lib/
ディレクトリの下にあるファイルを読み込む場合Gemfile
でrequire: false
が指定されているgem依存を読み込む場合これまでに、「ルーティング」「コントローラ」「アクション」「ビュー」について解説しました。これらはMVC(Model-View-Controller)と呼ばれるパターンに沿ったWebアプリケーションの典型的な構成要素です。MVCは、アプリケーションの責務を分割して理解しやすくするデザインパターンです。Railsは、このデザインパターンに従う規約になっています。
コントローラと、それに対応して動作するビューを作成したので、次の構成要素である「モデル」を生成しましょう。
モデル(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つを中心に解説します。
マイグレーション(migration)は、アプリケーションのデータベース構造を変更するときに使われる機能です。RailsアプリケーションのマイグレーションはRubyコードで記述するので、データベースの種類を問わずにマイグレーションを実行できます。
db/migrate/
ディレクトリの下に生成されたマイグレーションファイルを開いてみましょう。
class CreateArticles < ActiveRecord::Migration[7.2] 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
のブロック内には、title
とbody
という2つのカラムが定義されています。これらのカラムは、先ほど実行したbin/rails generate model Article title:string body:text
コマンドで指定したので、ジェネレータによって自動的に追加されました。
ブロックの末尾行はt.timestamps
メソッドを呼び出しています。これはcreated_at
とupdated_at
という2つのカラムを追加で定義します。後述するように、これらのカラムはRailsによって自動で管理されるので、モデルオブジェクトを作成・更新すると、これらのカラムに値が自動で設定されます。
それでは以下のコマンドでマイグレーションを実行しましょう。
$ bin/rails db:migrate
マイグレーションコマンドを実行すると、データベース上にテーブルが作成されます。
== CreateArticles: migrating =================================== -- create_table(:articles) -> 0.0018s == CreateArticles: migrated (0.0018s) ==========================
マイグレーションについて詳しくは、Active Recordマイグレーションを参照してください。
これで、モデルを介してテーブルとやりとりできるようになりました。
Railsのコンソール機能を使って、モデルで少し遊んでみましょう。Railsコンソールは、Rubyのirb
と同様の対話的コーディング環境ですが、irb
と違うのは、Railsとアプリケーションコードも自動的に読み込まれる点です。
以下を実行してRailsコンソールを起動しましょう。
$ bin/rails console
以下のようなRailsコンソールのプロンプトが表示されるはずです。
Loading development environment (Rails 7.2.0) blog(dev)>
このプロンプトで、先ほど作成したArticle
オブジェクトを以下のように初期化できます。
blog(dev)> article = Article.new(title: "Hello Rails", body: "I am on Rails!")
ここで重要なのは、このオブジェクトは単に初期化されただけの状態であり、まだデータベースに保存されていないことです。つまり、このオブジェクトはこのコンソールでしか利用できません(コンソールを終了すると消えてしまいます)。オブジェクトをデータベースに保存するには、save
メソッドを呼び出さなくてはなりません。
blog(dev)> 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
オブジェクトをもう一度表示すると、先ほどと何かが変わっていることがわかります。
blog(dev)> 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">
オブジェクトにid
、created_at
、updated_at
という属性(attribute)が設定されています。先ほどオブジェクトをsave
したときにRailsが追加してくれたのです。
この記事をデータベースから取り出したければ、そのモデルでfind
メソッドを呼び出し、その記事のid
を引数として渡します。
blog(dev)> 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
メソッドを呼び出します。
blog(dev)> 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というパズルの最後のピースです。次は、これらのピースをつなぎ合わせてみましょう。
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(Embedded Ruby)も書かれています。ERBとは、ドキュメントに埋め込まれたRubyコードを評価するテンプレートシステムのことです。
ここでは、<% %>
と<%= %>
という2種類のERBタグが使われています。<% %>
タグは「この中のRubyコードを評価する」という意味です。<%= %>
タグは「この中のRubyコードを評価し、返された値を出力する」という意味です。
これらのERBタグの中には、通常のRubyプログラムで書けるコードなら何でも書けますが、読みやすさのため、ERBタグにはなるべく短いコードを書く方がよいでしょう。
上のコードでは、@articles.each
が返す値は画面に出力したくないので<% %>
で囲んでいますが、(各記事の)article.title
が返す値は画面に出力したいので<%= %>
で囲んでいます。
ブラウザでhttp://localhost:3000を開くと最終的な結果を確認できます(bin/rails server
を実行しておくことをお忘れなく)。このときの動作は以下のようになります。
GET http://localhost:3000
というリクエストをサーバーに送信する。ArticlesController
のindex
アクションに割り当てる。index
アクションは、Article
モデルを用いてデータベースからすべての記事を取り出す。app/views/articles/index.html.erb
ビューをレンダリングする。これでMVCのピースがすべてつながり、コントローラに最初のアクションができました。このまま次のアクションを作ってみましょう。
ほぼすべてのWebアプリケーションは、何らかの形でCRUD(Create、Read、Update、Delete)操作を行います。Webアプリケーションで行われる処理の大半はCRUDです。Railsフレームワークはこの点を認識しており、CRUDを行うコードをシンプルに書ける機能を多数備えています。
それでは、アプリケーションに機能を追加してこれらの機能を探ってみましょう。
現在のビューは、データベースにある記事をすべて表示します。今度は、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
がキャプチャされ、ArticlesController
のshow
アクションでparams[:id]
と書くとアクセスできます。
それでは、show
アクションをapp/controllers/articles_controller.rb
のindex
アクションの下に追加しましょう。
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>
ここまでに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のルーティングを参照してください。
次はCRUDのC(Create)です。典型的なWebアプリケーションでは、リソースを1個作成するのに複数のステップを要します。最初にユーザーがフォーム画面をリクエストします。次にユーザーがそのフォームに入力して送信します。エラーが発生しなかった場合はリソースが作成され、リソース作成に成功したことを何らかの形で表示します。エラーが発生した場合はフォーム画面をエラーメッセージ付きで再表示し、フォーム送信の手順を繰り返します。
Railsアプリケーションでは、通常これらのステップを実現するときにnew
アクションとcreate
アクションを組み合わせて扱います。それでは2つのアクションをapp/controllers/articles_controller.rb
のshow
アクションの下に典型的な実装として追加してみましょう。
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
)。なお、このときの記事タイトルと本文にはダミーの値が使われます。これらはフォームが作成された後でユーザーが変更することになります。
render
メソッドが現在のリクエストに対するレスポンスとして指定のビューをレンダリングする一方で、redirect_to
メソッドはブラウザに新しいリクエストを発生させます。ここで重要なのは、redirect_to
メソッドはデータベースやアプリケーションのステートが変更された「後で」呼び出すべきであるという点です。ステートが変更される前にredirect_to
を呼び出すと、ユーザーがブラウザをリロードしたときに同じリクエストが再送信され、変更が重複してしまいます。
ここでは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
のブロック内でフォームビルダーのlabel
やtext_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 フォームヘルパーを参照してください。
送信されたフォームのデータは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を参照してください。
これまで見てきたように、リソースの作成は単独のステップではなく、複数のステップで構成されています。その中には、無効なユーザー入力を適切に処理することも含まれます。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
を変更してtitle
やbody
のエラーメッセージが表示されるようにしましょう。
<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 バリデーション § バリデーションエラーに対応するを参照してください。
これで、ブラウザで 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 %>
ここまでで、CRUDのうちCとRを実現しました。今度はUの部分、つまり更新を実装してみましょう。リソースの更新は、ステップが複数あるという点でリソースの作成と非常に似ています。最初に、ユーザーはデータを編集するフォームをリクエストします。次に、ユーザーがフォームにデータを入力して送信します。エラーが発生しなければ、リソースは更新されます。エラーが発生した場合はフォームをエラーメッセージ付きで再表示し、同じことを繰り返します。
更新のステップは、コントローラのedit
アクションとupdate
アクションで扱うのが慣例です。それでは、app/controllers/articles_controller.rb
の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 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
をレンダリングし、同じフォームをエラーメッセージ付きで再表示します。
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
のフォームと同じですが、すべての@article
をarticle
に置き換えてある点にご注目ください。パーシャルのコードは共有されるので、特定のインスタンス変数に依存しないようにするのがベストプラクティスです(コントローラのアクションで設定されるインスタンス変数に依存すると、他で使い回すときに不都合が生じます)。代わりに、記事をローカル変数としてパーシャルに渡します。
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 %>
パーシャルについて詳しくは、レイアウトとレンダリング § パーシャルを使うを参照してください。
これで、記事の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>
いよいよCRUDのDまで到達しました。リソースの削除はリソースの作成や更新よりもシンプルなので、必要なのは削除用のルーティングとコントローラのアクションだけです。削除用のルーティングは、DELETE /articles/:id
リクエストをArticlesController
の destroy
アクションに対応付けます。
それでは、app/controllers/articles_controller.rb
のupdate
アクションの下に典型的な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-method
とdata-turbo-confirm
を設定しています。どちらの属性も、新しいRailsアプリケーションにデフォルトで含まれているTurboにフックします。
data-turbo-method="delete"
を指定すると、GET
リクエストではなくDELETE
リクエストが送信されます。
data-turbo-confirm="Are you sure?"
を指定すると、リンクをクリックしたときに「Are you sure?」ダイアログが表示され、ユーザーが「キャンセル」をクリックするとリクエストを中止します。
以上でできあがりです!記事のリスト表示も、個別表示も、作成も、更新も、削除も思いのままです。CRUDバンザイ!
今度はアプリケーションに第2のモデルを追加しましょう。第2のモデルは記事へのコメントを扱います。
先ほどArticle
モデルの作成に使ったのと同じジェネレーターを見てみましょう。今回は、Article
モデルへの参照を保持するComment
モデルを作成します。ターミナルで以下のコマンドを実行します。
$ bin/rails generate model Comment commenter:string body:text article:references
コマンドを実行すると、以下の4つのファイルが作成されます。
ファイル | 目的 |
---|---|
db/migrate/<タイムスタンプ>_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.2] 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型カラムとそのインデックス、そしてarticles
のid
カラムを指す外部キー制約を設定します。それではマイグレーションを実行しましょう。
$ bin/rails db:migrate
Railsは、これまで実行されていないマイグレーションだけを適切に判定して実行するので、以下のようなメッセージだけが表示されるはずです。
== CreateComments: migrating ================================================= -- create_table(:comments) -> 0.0115s == CreateComments: migrated (0.0119s) ========================================
Active Recordの関連付け機能により、2つのモデルの間にリレーションシップを簡単に宣言することができます。今回の記事とコメントというモデルの場合、以下のいずれかの方法で関連付けを設定できます。
そして上の方法(における英語の記述)は、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の関連付けガイドを参照してください。
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のルーティングガイドを参照してください。
モデルを手作りしたので、モデルに合うコントローラも作ってみたくなります。それでは、再びこれまでと同様にジェネレータを使ってみましょう。
$ 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つ追加されています。このフォームはCommentsController
のcreate
アクションを呼び出すことでコメントを新規作成します。form_with
呼び出しでは配列を1つ渡しています。これは/articles/1/comments
のような「ネストしたルーティング(nested route)」を生成します。
今度はapp/controllers/comments_controller.rb
のcreate
アクションを改造しましょう。
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)
ヘルパーを用いて元の記事の画面に戻ります。既に説明したように、このヘルパーを呼び出すとArticlesController
のshow
アクションが呼び出され、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 %>
これで、ブログに記事やコメントを自由に追加して、それらを正しい場所に表示できるようになりました。
さて、ブログの記事とコメントが動作するようになったので、ここでapp/views/articles/show.html.erb
テンプレートを見てみましょう。何やらコードがたくさん書かれていて読みにくくなっています。ここでもパーシャルを使ってコードをきれいにしましょう。
最初に、特定記事のコメントをすべて表示する部分を切り出してコメントパーシャルを作成しましょう。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
というローカル変数が使われるので、これをパーシャルの表示に利用できます。
今度はコメント作成部分もパーシャルに追い出してみましょう。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", article: @article %>
2番目のrender
は、レンダリングするパーシャルテンプレートをcomments/form
という形で指定しているだけです。comments/form
と書くだけで、Railsは区切りのスラッシュ文字を認識し、app/views/comments
ディレクトリの_form.html.erb
パーシャルをレンダリングすればよいということを理解し、実行してくれます。app/views/comments/_form.html.erb
などと書く必要はありません。
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>
スパムコメントを削除できるようにするのも、このブログでは重要な機能です。そのためのビューを作成し、CommentsController
にdestroy
アクションを作成する必要があります。
最初に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
アクションに戻ります。
ある記事を削除したら、その記事に関連付けられているコメントも一緒に削除する必要があります(そうしないと、コメントがいつまでもデータベース上に残ってしまいます)。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
このブログアプリケーションをオンラインで公開すると、このままでは誰でも記事を追加/編集/削除したり、コメントを削除したりできてしまいます。
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
コメントの削除も認証済みユーザーにだけ許可したいので、CommentsController
(app/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認証ダイアログが表示されます。
正しいユーザー名とパスワードを入力すると、別のユーザー名とパスワードが要求されるか、ブラウザが閉じられるまで、認証された状態が続きます。
もちろん、Railsでは他の認証方法も使えます。Railsにはさまざまな認証システムがありますが、その中で人気が高い認証システムはDeviseとAuthlogic gemの2つです。
セキュリティ、それもWebアプリケーションのセキュリティは非常に幅広く、かつ詳細に渡っています。Railsアプリケーションのセキュリティについて詳しくは、本ガイドのRailsセキュリティガイドを参照してください。
以上で、Railsアプリケーションを初めて作るという試みは終わりです。この後は自由に更新したり実験を重ねたりできます。
もちろん、何の助けもなしにWebアプリケーションを作らなければならないなどということはないということを忘れてはなりません。RailsでWebアプリを立ち上げたり実行したりするうえで助けが必要になったら、以下のサポート用リソースを自由に参照できます。
Railsでの無用なトラブルを避けるための最も初歩的なコツは、外部データを常にUTF-8エンコーディングで保存しておくことです。そうしておかないと、RubyライブラリやRailsがネイティブデータをたびたびUTF-8に変換しなければならず、しかも場合によっては失敗します。外部データのエンコーディングは常にUTF-8で統一することをおすすめします。
外部データのエンコードが統一されていないと、たとえば画面に黒い菱型◆
や疑問符?
が表示されたり、"ü"という文字のはずが"ü"という文字に化けたりするといった症状がよく発生します。Railsではこうした問題を緩和するため、問題の原因を自動的に検出して修正するために内部で多くの手順を行っています。しかし、UTF-8で保存されていない外部データがあると、Railsによる自動検出・修正が効かないデータで文字化けが発生することがあります。
UTF-8でないデータの主な原因は以下の2つです。
◆?
のようにブラウザ表示が文字化けすることがあります。これはi18n(国際化)用の翻訳ファイルで発生することもあります。DreamweaverのようにUTF-8保存がデフォルトでないエディタであっても、デフォルトをUTF-8に変更する方法は用意されているはずです。エンコードをUTF-8に変更してください。Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。