<y>

Splatoonプレイヤー兼Webエンジニア

日報 2016/07/13 -Ruby on Railsのform_forとform_tagの違いで悩んだ-

今日ちょっと業務でハマったところがあったのでそれについて書く。

form_forとform_tagについて

作っているやつ

転職して二週間弱経ったが、自分の理解力の無さと知識吸収力の低さに辟易しつつ、Ruby on Railsを使って業務をしている。
Viewの部分をhtml.erbを使って書いているのだが、「アカウント新規登録の際のパスワード」と「確認用パスワード」の二つの項目をControllerにPOSTで送信する際に、どうしてもエラーが消えないという躓きに出会った。
値の受け渡しの流れは以下の通り。

  1. View(html.erb)で"新規パスワード"と"確認パスワード"の二つの項目をPOST形式で送信し、Controllerでパラメータとして受け取る
  2. strong parametersを利用して「新規パスワード」「確認パスワード」のみをpermitする
  3. 受け取った二つの値を比較し、一致していればモデルのインスタンスにパスワードを設定してsaveメソッドを呼び出す
  4. 値が異なっていれば、エラーメッセージを設定し、新規・確認パスワードの入力画面に戻る

具体的なコードはこんな感じ。

html.erb

<%= form_for(@model, action: :index, method: :post) do |f| %>
  <div class="new_password">
    <%= f.label(:new_password,"パスワード新規登録") %>
    <%= f.password_field(:new_password) %><br>
  </div>
  <div class="confirm_password">
    <%= f.label(:confirm_password,"登録パスワード確認用") %>
    <%= f.password_field(:confirm_password) %><br>
  </div>
  <div class="password_submit">
    <%= submit "登録" %>
<% end %>
controller.rb

  def create
    param = strong_params
    @model.password = param[:new_password]
    @model.onetime_password = ""
    if param[:new_password] != param[:confirm_password]
      @model.errors.add(:base, "パスワードが一致しません")
      render :new
    elsif @model.save
      session[:model_id] = @model.id
      redirect_to :index
    else
      @model.error.add(:base, "エラーが発生しました")
      render :new
    end
  end

  private
  def strong_params
    params.permit(
      :new_password,
      :confirm_password
    )
  end

いざパスワード登録!と登録ボタンを押したところ、いわゆる「ぬるぽ」が発生した。 発生箇所が

elsif @model.save

の行だったのと、DB内でパスワードが入るカラムにはNOT NULL制約がかかっていたのもあり、「パラメータで受け取った二つのパスワードの値がnilっぽい」ことが分かった。

form_tagを使わなくてはいけなかった理由

原因が分からず唸りながらググッていたところ、こんな記事を見かけた。

qiita.com

html.erbでフォームを作成する際、form_forとform_tagという二つの選択肢がある。 正直違いがあんまり分からないし、前までform_for使って上手くいってたからform_for使おう、という軽い気持ちでform_forを使っていた。 簡潔に書くと、

  • form_forは第一引数で指定したモデルに基づいたフォームを作成する時に使う
  • form_tagはモデルに基づかないフォームを作成するときに使う

ということになる。
今回はパスワード新規登録とパスワード確認用の二つの値だけで、特に基づくモデルがないため、本来ならform_tagを使うべきだった。
そのため、以下のようにコードを修正した。

html.erb

<%= form_tag(action: :index, method: :post) do %>
  <div class="new_password">
    <%= label(:new_password,"パスワード新規登録") %>
    <%= password_field_tag(:new_password) %><br>
  </div>
  <div class="confirm_password">
    <%= label(:confirm_password,"登録パスワード確認用") %>
    <%= password_field_tag(:confirm_password) %><br>
  </div>
  <div class="password_submit">
    <%= submit_tag "登録" %>
<% end %>

こうすることで、無事にパスワードを登録することができるようになった。

form_forでもぶっちゃけ問題なく動作する

form_tagを利用してめでたしめでたしという感じだったのだが、ぬるぽが起きた具体的な理由は別のところにある。
順を追うと、

  1. ぬるぽが起きた原因としては、saveメソッドを呼んだ際に@model.passwordの中身がnilだったから
  2. @model.passwordの中身がnilなのは、strong_paramsでpermitされた値ではないから
  3. ではstrong_paramsでpermitに指定したnew_passwordとconfirm_passwordがpermitされていないのはなぜか

結論から言うと、strong parametersでネストを正しく表現していなかったからである。
form_forはモデルに基づいた値をPOSTするため、Controllerに送られる値は以下のようなネスト構造になる。

  • params
    • model
      • new_password
      • confirm_password

strong parametersでは、permitするパラメータがネストしている場合は、それを反映させなければならない。
つまり、

def strong_params
  params.permit(
    [:model][:new_password],
    [:model][:confirm_password]
  )

のように書かなくてはならない(もっとうまい書き方はあるだろうが…)。
パラメータがpermitされなかった理由は、ネストをしっかりと記述していなかったからである。
from_tagを使うと、

  • params
    • new_password
    • confirm_password

というネスト構造になるので、最初に書いていたstrong_paramsで問題ない。
結局、直し方としては

  • View(html.erb)をform_forからform_tagに修正する
  • strong_paramsのネスト構造を修正する

という二通りがある。
「html.erbでform_forをform_tagに直すよりstrong_paramsのネスト構造を直した方が楽じゃね?」と感じる方もいると思う。まったくもってその通りだ。
しかし、今回はあくまで「パスワードの登録」であり、「基づくモデルが存在しない」ことである。password.rbというモデルを作成して…とやっていたら、細分化しすぎて逆に分かりにくくなる。userというモデルにnameとmail_addressとageとpasswordいうプロパティがあり…という方が自然だ。大変分かりにくくて申し訳ないが、一画面でuserモデルの全てのプロパティを入力するフォームを作成するならform_forを利用すべきだが、今回は「新規登録パスワードと確認用パスワード」を入力するフォームだったため、モデルに基いていないということだ。
上記の点を考慮すると、form_forを利用してstrong_paramsを修正するのは、実質的には間違いと同義であるということになる、と考えられるのではないか。

まとめ

分からないことに対する知見が得られたときはとても嬉しい。ド素人なのもあって正直これで結構悩んだので、解決できたときはわりと嬉しかった。
今見返したら相当見にくい文章だということに気付いた。qiitaで読みやすい記事書いてる人、端的に言って尊敬する。