学習の備忘録として、AIを活用しながらまとめたアウトプット記事です。
正確な理解に努めていますが、お気づきの点やアドバイスなどございましたら、気軽にコメントいただけると助かります!
はじめに
Railsで掲示板などの投稿機能を学習する際、自分自身が params 周りや Strong Parameters の仕組みに苦手意識があり、なかなかすんなり理解できませんでした。
そこで、1行のコードの意味や背景を根本から整理し、初学者でも直感的に理解できるようにアウトプットとしてまとめてみました。
同じような方の参考になれば幸いです。
Railsで投稿作成機能(create アクション)を実装すると、必ずといっていいほど目にするこのコード。
@post = current_user.posts.build(post_params)
- なぜ
paramsをそのまま使わず、わざわざpost_paramsを挟むの? -
build(...)のカッコの中のpost_paramsって、変数? それともメソッド? -
Post.newじゃなくてcurrent_user.posts.buildなのはなぜ?
この記事では、この3つの疑問を「ダンボール箱と検問所」のイメージで整理します。
この記事を読み終えると、上のコード1行を自分の言葉で説明できるようになります。
対象読者:Rails の基本(MVC、フォーム、
createアクション)を学び始めた方
想定環境:Rails 8 系(Rails 7 以降なら同じ考え方で読めます)
結論(先に一言で)
| 登場人物 | たとえると | 正体 |
|---|---|---|
params |
ユーザーから届いた中身の分からないダンボール箱 | リクエストで送られてきた生のデータ |
post_params |
中身を確認して許可したものだけ取り出す検問所 |
private で定義したメソッド
|
current_user.posts.build(...) |
投稿者の名札を付けて新しい投稿を準備する |
user_id が自動でセットされる |
処理の流れはこうです。
0. 前提:params の中身はこうなっている
投稿フォームで「タイトル」と「本文」を入力して送信すると、Rails の params にはこんな形でデータが入ります。
params
# => {
# "authenticity_token" => "...",
# "post" => { "title" => "こんにちは", "body" => "初投稿です" },
# "commit" => "登録する"
# }
ポイントは2つです。
- フォームの項目は
postというキーの中にまとまっています(form_with model: @postを使うとこうなります) -
authenticity_tokenやcommitのような、投稿に関係ないデータも一緒に届きます
つまり params は、欲しいものも不要なものも全部入った箱です。
(厳密には ActionController::Parameters というオブジェクトですが、ここではハッシュのようなものと考えて大丈夫です)
1. なぜ params をそのまま使ってはいけないの?
悪意のあるユーザーはフォームにない項目も送れる
フォームには「タイトル」と「本文」の入力欄しかありません。
しかし、ブラウザの開発者ツールや curl コマンドを使えば、フォームにない項目を勝手に付け足して送信できます。
ダンボール箱の中身 (params[:post]):
・title: "こんにちは"
・body: "初投稿です"
・user_id: 999 ← フォームに無いのに、他人のIDを勝手に追加された!
もし、これをそのままデータベースに保存できてしまうと、他人になりすました投稿が作れてしまいます。
(ユーザー登録フォームなら、admin: true を紛れ込ませて管理者になるという攻撃も考えられます)
このように、送られてきたデータを、そのままモデルに丸ごと渡して意図しない項目まで書き換えられてしまう問題を Mass Assignment(マスアサインメント)脆弱性と呼びます。
Rails 3 以前はこの対策が不十分で、2012年には GitHub でも実際に問題になりました。その反省から、Rails 4 以降は次の仕組みが標準になっています。
今の Rails は、危険なデータをそのまま渡すとエラーで止めてくれる
# ❌ NG:params をそのまま渡す
@post = Post.new(params[:post])
Rails 4 以降では、こう書いて実行すると次のエラーになります。
ActiveModel::ForbiddenAttributesError
「許可(permit)されていないデータは、モデルに渡させないよ」という Rails 側の安全装置です。
エラーが出るのは面倒に見えますが、うっかり危険なコードを書いても事故にならないように守ってくれているということです。
2. 解決策:検問所(Strong Parameters)
では、どうすれば安全にデータを渡せるのか。
答えが、コントローラーの private に書いてある post_params メソッドです。
private
# 🔍 検問所:許可した項目だけを取り出すメソッド
def post_params
params.require(:post).permit(:title, :body)
end
この仕組みを Strong Parameters と呼びます。1行ずつ読んでみましょう。
| コード | 意味 |
|---|---|
params.require(:post) |
「post の箱を持ってきて」。post キーが無ければエラー(ActionController::ParameterMissing)になります |
.permit(:title, :body) |
「その中から title と body だけを許可して取り出して」 |
先ほどの user_id: 999 を送られても、permit に書いていない項目は取り除かれます。
開発中のログには、次のように警告が出ます。
Unpermitted parameter: :user_id
結果として post_params から返ってくるのは、こうなります。
post_params
# => { "title" => "こんにちは", "body" => "初投稿です" } ← 許可した項目だけ
「許可リスト(ホワイトリスト)方式」なのがポイントです。
「危険なものを禁止する」のではなく、「安全だと決めたものだけ通す」ので、想定外の項目が来ても安全です。
【発展】Rails 8からはもっとシンプルに書ける!
想定環境に「Rails 8系」と書きましたが、実はRails 8からStrong Parametersの新しい書き方として expect メソッド が導入されました。
# 従来の書き方 (Rails 7以前〜)
def post_params
params.require(:post).permit(:title, :body)
# Rails 8 から導入された新しい書き方 ✨
def post_params
params.expect(post: [:title, :body])
end
今までは require(必須チェック)と permit(許可リスト)の2ステップが必要でしたが、expect を使うと「post というキーが必須で、その中身は title と body を許可する」という処理が 1ステップでより安全に 書けるようになりました。
これから新しくRails 8でアプリを作る方は、ぜひこの最新の書き方も試してみてください!
3. current_user.posts.build(post_params) を分解する
ここまでの知識で、1行を左から順に読んでみましょう。
@post = current_user.posts.build(post_params)
| パーツ | 意味 |
|---|---|
current_user |
いまログインしているユーザー |
.posts |
そのユーザーが持っている投稿(has_many :posts で使えるようになる関連) |
.build(...) |
新しい Post を作る(まだ保存はしない)
|
post_params |
検問所を通った、安全な title と body
|
日本語にすると、ログイン中のユーザーの投稿として、検問を通ったタイトルと本文で、新しい投稿を準備する、という意味です。
create アクション全体
class PostsController < ApplicationController
def create
# ① 検問所を通したデータで、ログインユーザーの投稿を準備する
@post = current_user.posts.build(post_params)
# ② 保存を試みる
if @post.save
redirect_to posts_path, success: '投稿を作成しました'
else
flash.now[:danger] = '投稿を作成できませんでした'
render :new, status: :unprocessable_entity
end
end
private
# ③ 検問所
# 従来の書き方 (Rails 7以前〜)
def post_params
params.require(:post).permit(:title, :body)
end
# Rails 8 から導入された新しい書き方 ✨
# def post_params
# params.expect(post: [:title, :body])
# end
end
実行される順番は ③(post_params が呼ばれる)→ ①(build)→ ②(save) です。
build に渡す引数を作るために、先に post_params メソッドが動いて、その戻り値が build に渡されます。
4. 3つの「へぇ〜!」ポイント
① カッコの中の post_params は変数? メソッド?
メソッドです。
Rubyは、メソッドを呼び出すときの () を省略できます。今回のコードを省略せずに書くと、実はこうなっています。
@post = current_user.posts.build(post_params())
「post_params という引数のないメソッドを実行し、その戻り値を build メソッドの引数として渡している」という構造です。
なお、post_params を private に書くのは、アクションとして外部から呼ばれないようにするためです。
コントローラーの public メソッドは URL からアクセスされうる「アクション」になりますが、post_params は内部で使う補助メソッドなので、private に隠します。
② なぜ Post.new ではなく current_user.posts.build なの?
Post.new(post_params) # user_id が空のまま
current_user.posts.build(post_params) # user_id にログインユーザーのIDが自動でセットされる
Post.new だと、「誰の投稿か」を表す user_id を自分で入れる必要があります。
@post = Post.new(post_params)
@post.user_id = current_user.id # ← 毎回これを書く必要がある
current_user.posts.build を使えば、この手間が不要です。
さらに、user_id をフォームから受け取らず、サーバー側のログイン情報から決めるので、先ほどのなりすまし対策にもなります。
(permit に :user_id を書かないのも、同じ理由です)
③ status: :unprocessable_entity って何?
入力エラー(タイトルが空など)のときに render :new で入力画面に戻す場合、Rails 7 以降(Turbo を使う環境)では、HTTP ステータス 422(入力内容に問題がある)を付けて返す 必要があります。
付けないと、エラーメッセージ付きの画面が表示されません。
Turbo は、フォーム送信(POST など)が成功したらリダイレクトで結果ページへ移動する ことを前提にしています。
これは、POST の結果をそのまま表示するとリロード時に同じ送信が繰り返され、二重投稿になる、という問題を避けるための定番の作法(PRG パターン)に沿ったものです。
一方、保存に失敗したときは入力画面をその場で再表示したいので、「これはエラーの応答です」と Turbo に伝えるために、422 を付けます。
まとめ 🎈
-
params= 何が入っているか分からない、ユーザーから届いた生のデータ -
post_params= 許可した項目だけを取り出す検問所(Strong Parameters のメソッド) -
current_user.posts.build(post_params)= 検問を通った安全なデータで、ログインユーザーの投稿を準備する - 許可していないデータをそのまま渡すと、Rails が
ForbiddenAttributesErrorで止めてくれる
Rails のコードは、「誰の投稿を、どんな検問を通したデータで、作っているのか」というストーリーで読むと、1行ずつ意味が見えてきます。
今回の1行は、MVC でいうと「フォームから届いたデータ(View)を、Controller で検問して、Model(Post)として保存する」流れの真ん中にあたります。この全体像を意識しながらコードを読むと、さらに理解が深まります!