第14章|今さら学ぶ「コントローラ詳解」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- 7つのアクション(index〜destroy)の責務を正確に理解する
-
before_action— アクション実行前のセキュリティゲート -
redirect_toとrenderの決定的な違い - フラッシュメッセージ — 一度だけ表示される伝言板
- Strong Parameters — 「許可証のない荷物は通さない」仕組み
- KnowledgeNoteの ArticlesController を完全に読み解く
🏠 たとえ話で掴む「コントローラ」
第9章でコントローラを「レストランのホールスタッフ」にたとえました。今回はもう少し具体的に、 市役所の窓口担当者 として深掘りします。
窓口担当者の1日は、こんな流れです。
- 来庁者が窓口に来る (リクエストが届く)
-
身分証を確認する (
before_actionで認証チェック) - 担当部署に書類を依頼する (モデルにデータを要求)
- 書類を受け取って渡す (ビューにデータを渡してレスポンス)
- 案内表示を出す (フラッシュメッセージ)
大事なのは、窓口担当者は 自分で書類を作らない ということ。バックオフィス(モデル)に頼んで、受け取ったものをお客さん(ブラウザ)に渡すだけです。
コントローラとは何か — 技術的な定義
MVCにおけるコントローラの役割
コントローラ(Controller) は、MVCアーキテクチャの中でリクエストを受け取り、モデルとビューを橋渡しする層です。
具体的には、ルーティング(第10章)から振り分けられたリクエストを受け取り、必要に応じてモデルにデータの取得・保存を依頼し、その結果をビューに渡してレスポンスを返します。コントローラ自身はビジネスロジックを持たず、 処理の交通整理 に徹するのが原則です。
Railsのコントローラは ApplicationController を継承したクラスで、ActionController::Base が提供するリクエスト処理・レスポンス生成・セッション管理などの機能を使えます。
なぜ「薄いコントローラ」が良いのか
コントローラに複雑なロジックを書くと「Fat Controller(太ったコントローラ)」になり、テストが困難で変更にも弱くなります。ロジックはモデルやサービスクラスに任せ、コントローラは「受け取って → 頼んで → 返す」の3ステップに留めるのが理想です(→ 第25章で詳しく扱います)。
📋 7つのアクション — 窓口業務の全メニュー
第10章で resources が7つのルートを生むと学びました。今度はその受け口となる 7つのアクション をコントローラ側から見ます。
class ArticlesController < ApplicationController
# ① index — 一覧を見せる
def index
@articles = Article.published.recent.page(params[:page])
end
# ② show — 1件の詳細を見せる
def show
@article = Article.find(params[:id])
end
# ③ new — 作成フォームを表示する(まだ保存しない)
def new
@article = Article.new
end
# ④ create — フォームのデータを受け取って保存する
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: "記事を投稿しました"
else
render :new, status: :unprocessable_entity
end
end
# ⑤ edit — 編集フォームを表示する(まだ保存しない)
def edit
@article = Article.find(params[:id])
end
# ⑥ update — 編集データを受け取って更新する
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article, notice: "記事を更新しました"
else
render :edit, status: :unprocessable_entity
end
end
# ⑦ destroy — 削除する
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to articles_path, notice: "記事を削除しました", status: :see_other
end
private
def article_params
params.expect(article: [:title, :body, :published])
end
end
7つのアクション早見表
| アクション | HTTPメソッド | 役割 | ビューが必要か |
|---|---|---|---|
index |
GET | 一覧表示 | ✅ index.html.erb |
show |
GET | 詳細表示 | ✅ show.html.erb |
new |
GET | 作成フォーム表示 | ✅ new.html.erb |
create |
POST | 保存処理 | ❌(成功→redirect、失敗→render :new) |
edit |
GET | 編集フォーム表示 | ✅ edit.html.erb |
update |
PATCH | 更新処理 | ❌(成功→redirect、失敗→render :edit) |
destroy |
DELETE | 削除処理 | ❌(redirect) |
create / update / destroy は専用のビューを持ちません。処理が終わったら別のページにリダイレクトするか、フォームを再表示します。
🚧 before_action — 窓口に入る前のセキュリティゲート
before_action は、アクションが実行される前に自動で呼ばれる処理です。市役所のセキュリティゲートのように、「窓口に来る前に身分証を確認する」役割を果たします。
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authorize_author!, only: [:edit, :update, :destroy]
def index
@articles = Article.published.recent.page(params[:page])
end
def show
# @article は before_action でセット済み
end
def edit
# @article のセットも権限チェックも before_action で完了済み
end
# ...
private
def set_article
@article = Article.find(params[:id])
end
def authenticate_user!
unless current_user
redirect_to login_path, alert: "ログインしてください"
end
end
def authorize_author!
unless @article.user == current_user
redirect_to root_path, alert: "権限がありません"
end
end
end
before_action の実行順序
リクエスト到着
↓
① authenticate_user!(ログインチェック)
↓ パスしたら
② set_article(記事データの取得)
↓ パスしたら
③ authorize_author!(権限チェック)
↓ パスしたら
④ アクション本体(edit / update 等)
before_action は 上から順に実行 されます。途中で redirect_to が呼ばれると、それ以降の処理は実行されません。セキュリティゲートで止められたら窓口までたどり着けない、という仕組みです。認証の詳細(Deviseを使った実装)は(→ 第21章で詳しく扱います)。
only と except
before_action :authenticate_user!, except: [:index, :show]
# → index と show 以外のアクションで実行(= 一覧・詳細は未ログインでも見れる)
before_action :set_article, only: [:show, :edit, :update, :destroy]
# → show, edit, update, destroy のときだけ実行
🔀 redirect_to と render の違い
コントローラでつまずきやすいのが、redirect_to と render の違いです。これを 窓口での案内 にたとえます。
redirect_to — 「別の窓口へ行ってください」
redirect_to articles_path
# → ブラウザに「/articles に改めてアクセスしてね」と伝える
# → ブラウザは新しいリクエスト(GET /articles)を送る
# → 2回目のリクエスト・レスポンスが発生する
redirect_to は「別の窓口(URL)へ案内する」動作です。ブラウザは新しいリクエストを送り直すので、 URLが変わります。
render — 「その場で書類を渡す」
render :new
# → 今のリクエストの中で new.html.erb を描画する
# → 新しいリクエストは発生しない
# → URLは変わらない
render は「その場でビューを組み立てて返す」動作です。新しいリクエストは発生しないので、 URLは変わりません。
図で見る違い
【redirect_to の場合】
ブラウザ → POST /articles(create失敗)
← 302 Redirect to /articles/new
ブラウザ → GET /articles/new(新しいリクエスト)
← 200 OK(new.html.erb)
※ 2往復する。@article のエラー情報は消える!
【render :new の場合】
ブラウザ → POST /articles(create失敗)
← 422 Unprocessable Entity(new.html.erb をその場で返す)
※ 1往復で済む。@article のエラー情報が残る!
create / update の定型パターン
def create
@article = current_user.articles.build(article_params)
if @article.save
# 保存成功 → 別ページにリダイレクト
redirect_to @article, notice: "記事を投稿しました"
else
# 保存失敗 → フォームを再表示(renderでエラー情報を保持)
render :new, status: :unprocessable_entity
end
end
成功したら redirect_to、失敗したら render。 これはRailsの定型パターンです。
失敗時に redirect_to を使うと、@article.errors(エラー情報)が次のリクエストまで保持されず、エラーメッセージが表示されなくなります。だから失敗時は render を使います。
📢 フラッシュメッセージ — 一度だけ表示される伝言板
フラッシュ は、次のリクエストで一度だけ表示されるメッセージです。市役所の窓口で「手続き完了しました」と掲示板に一瞬だけ表示されるイメージです。
# 成功メッセージ
redirect_to @article, notice: "記事を投稿しました"
# ↑ notice は flash[:notice] のショートカット
# 警告メッセージ
redirect_to root_path, alert: "権限がありません"
# ↑ alert は flash[:alert] のショートカット
# render のときは flash.now を使う
flash.now[:alert] = "入力内容に問題があります"
render :new, status: :unprocessable_entity
flash と flash.now の違い
flash |
flash.now |
|
|---|---|---|
| 表示タイミング | 次の リクエストで表示 | 今の リクエストで表示 |
| 使う場面 |
redirect_to と一緒に |
render と一緒に |
| 理由 | redirectは新しいリクエストが来るから | renderは今のリクエスト内で完結するから |
<!-- app/views/layouts/application.html.erb — フラッシュの表示 -->
<% if flash[:notice] %>
<div class="bg-green-50 text-green-700 p-4 rounded mb-4">
<%= flash[:notice] %>
</div>
<% end %>
<% if flash[:alert] %>
<div class="bg-red-50 text-red-700 p-4 rounded mb-4">
<%= flash[:alert] %>
</div>
<% end %>
🛡️ Strong Parameters — 許可証のない荷物は通さない
なぜコントローラでパラメータを制限するのか
コントローラの責務は「外から来たリクエストを受け取り、モデルに引き渡す」ことです。ここで重要なのが、 外から来たデータをそのまま信用してはいけない という原則です。
ブラウザから送られるデータには、悪意のあるパラメータが紛れ込む可能性があります。
# 攻撃者がフォームを改ざんして、こんなデータを送ってきたら?
{
article: {
title: "普通の記事",
body: "普通の本文",
user_id: 999 # ← 著者を別人に変更しようとしている!
}
}
もし全てのパラメータをそのまま受け取ると、user_id まで更新されてしまいます。この問題は マスアサインメント脆弱性 と呼ばれ、Rails 3以前は実際に大きなセキュリティ問題を引き起こしました(→ 第20章で詳しく扱います)。
Strong Parameters は、コントローラ層でリクエストパラメータをallowlist方式で制限する仕組みです。「この項目だけ受け取ってOK」と許可リストを作ることで、余分なパラメータの混入を防ぎます。
Rails 8.0 の params.expect
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
# ...
private
# Rails 8.0 の推奨方式
def article_params
params.expect(article: [:title, :body, :published])
# ↑ title, body, published だけを許可。それ以外は弾く
end
end
💡 Rails 7 との違い
Rails 7 ではparams.require(:article).permit(:title, :body, :published)と書きます。Rails 8.0 ではparams.expect(article: [:title, :body, :published])が推奨です。expectはキーが存在しない場合にActionController::ParameterMissingを発生させるため、セキュリティ面でより安全です。どちらも動作しますが、新規プロジェクトではexpectを使います。
ネストしたパラメータ
タグのように配列データを受け取る場合は、こう書きます。
def article_params
params.expect(article: [:title, :body, :published, tag_ids: []])
# tag_ids: [] — 配列を許可する
end
privateメソッドとして定義する理由
article_params は private の下に書きます。これはアクションとして外部から呼び出されるメソッドではなく、コントローラ内部の補助メソッドだからです。コントローラのpublicメソッドはRailsにアクションとして認識されるため、内部ヘルパーはprivateに置くのがルールです。
🛠️ KnowledgeNoteでの具体例
KnowledgeNoteの ArticlesController を、ここまでの知識を総動員した完成形にします。
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authorize_author!, only: [:edit, :update, :destroy]
# GET /articles
def index
@articles = Article.published.recent.page(params[:page])
end
# GET /articles/:id
def show
# @article は before_action でセット済み
end
# GET /articles/new
def new
@article = Article.new
end
# POST /articles
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: "記事を投稿しました"
else
flash.now[:alert] = "入力内容に問題があります"
render :new, status: :unprocessable_entity
end
end
# GET /articles/:id/edit
def edit
# @article は before_action でセット済み
end
# PATCH /articles/:id
def update
if @article.update(article_params)
redirect_to @article, notice: "記事を更新しました"
else
flash.now[:alert] = "入力内容に問題があります"
render :edit, status: :unprocessable_entity
end
end
# DELETE /articles/:id
def destroy
@article.destroy
redirect_to articles_path, notice: "記事を削除しました", status: :see_other
end
private
def set_article
@article = Article.find(params[:id])
end
def authorize_author!
unless @article.user == current_user
redirect_to root_path, alert: "権限がありません"
end
end
def article_params
params.expect(article: [:title, :body, :published])
end
end
💼 面接で聞かれたら?
Q:redirect_to と render の違いを説明してください。
「
redirect_toはブラウザに別のURLへアクセスし直すよう指示するので、新しいリクエストが発生しURLが変わります。renderは現在のリクエスト内でビューを描画して返すので、新しいリクエストは発生せずURLは変わりません。保存成功時はredirect_toでページ遷移、バリデーション失敗時はrenderでフォームを再表示してエラーメッセージを保持するのが定型パターンです。」深掘りされたら:
- 「before_action とは?」→ アクション実行前に自動で呼ばれるフィルター。認証チェックやデータ取得など、複数アクションに共通する処理をDRYに書ける。
- 「flashとflash.nowの違いは?」→ flashは次のリクエストで表示(redirect_to時)。flash.nowは今のリクエストで表示(render時)。
Q:Strong Parametersとは何ですか?
「Strong Parametersは、コントローラでリクエストパラメータをホワイトリスト方式で制限する仕組みです。フォームから意図しないパラメータ(user_idやadminフラグなど)が送り込まれる「マスアサインメント脆弱性」を防ぎます。Rails 8.0では
params.expectで許可する項目を明示します。」深掘りされたら:
- 「params.expect と params.require.permit の違いは?」→ どちらもStrong Parametersの実装。
expectはRails 8.0で推奨されており、キーが存在しない場合にActionController::ParameterMissingを発生させるためより安全。
🔗 もっと深く知りたい人へ(1次情報リンク)
- Rails ガイド:Action Controller の概要 — フィルター・セッション・フラッシュの全解説
- Rails ガイド:Strong Parameters — パラメータの許可と制限
- Rails API:ActionController::Redirecting — redirect_to のオプション
- Rails API:ActionController::Rendering — render のオプション
まとめ
- ✅ 7つのアクション(index/show/new/create/edit/update/destroy)がコントローラの基本
- ✅
before_actionでアクション前の共通処理(認証・データ取得)をDRYに書ける - ✅ 成功したら
redirect_to(URL変わる)、失敗したらrender(URL変わらない、エラー保持) - ✅ フラッシュは
redirect_toならflash、renderならflash.nowを使う - ✅ Strong Parametersでフォームからの不正パラメータを防ぐ。Rails 8.0では
params.expectを使う - ✅ コントローラは「窓口担当者」。自分で処理せず、モデルに頼んで結果を返す
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに