RailsのFormで配列を扱う

使用頻度が結構多いわりに、あまり詳しく書かれている本が見当たらないので、まとめメモ。

text_field等のヘルパーを使いつつ複数の要素を配列として取得したい時がある。リレーションでいうとhas_manyな要素をまとめて作成したい時とか。Helper使わずにHTMLタグ書いちゃえって思うこともあるけども、Helper使うとやっぱり楽だ。

(Helperを使わないときは↓のようになる)

<input type="text" name="book[0][name]">

今回は特に配列な要素を新規作成したいケースで。


例えば各ユーザー(User)がお気に入りの本(Book)を3個登録したいとき。
(Userがhas_many :booksで Bookがbelongs_to :user)
(今回はUserも同時に作成したい)

Controller

  def new
    @user = User.new
    @books = (1..3).map do
      Book.new
    end
  end

  def create
    user = User.create(params[:user])
    for book in params[:book]
      book[1].update(:user_id => user.id)
    end
    Book.create(params[:book].values)
  end

View

<% form_tag :action => :create do %>
あなたの名前とお気に入りの本を3冊書いてね

なまえ:<%= text_field :user, :name %><br />

<% @books.each_with_index do |@book, i| %>

本<%= i %>
<%= text_field "book[]", :name, :index => i %><br />

<% end %>
<br />
<%= submit_tag "Create" %>

<% end %>


ポイントは簡単でtext_fieldの第一引数のモデル名を各ところに[]を書くこと。
そうすると配列のIndexはモデルのIDになるんだけども、Updateではなく今回のように新規作成でNewしたばかりのActiveRecordオブジェクトではエラーになる。
そこで:indexオプションを指定することで明示的に配列のIndexを指定できる。
今回はeach_with_indexで単純に連番を振っている。
更新の場合はIndex指定の必要はない。

<%= text_field "book[]", :name, :index => i %>


それと事前にARオブジェクトを作っておく必要もある。@books = (1..3).map{Book.new}←ここは好きな数で。
作らなかったり、@モデル名(厳密にはモデル名でなくてもtext_field第一引数名とそのオブジェクトの変数名を同じ)にしておかないととこんなエラーが出る。

object[] naming but object param and @object var don't exist or don't respond to id_before_type_cast: nil

今回はbelongs_to :user_idをMySQLでNOT NULL制約をかけていて、Userのレコードも同時に作成しているので、まずUserレコードを作成したのちに、そのUserのIDをParamsにUpdateしている。
事前にuser_idが分かっている場合はHiddenで埋め込めばいいっぽい?(未検証
あるいはNOT NULL制約をかけてない場合はCreate後にUpdateすることも可能か。

そうした場合Createのコードはもう少しシンプルになって

  #Params内に親のIDが記録されている場合
  Book.create(params[:book].values)

  #Params内にはないけど、Create後にUpdateする場合(この方法ではCreate後に各要素にブロックが実行されるのでNOT NULL制約時はエラー)。
  Book.create(params[:book].values) do |book|
    book.user_id = user.id
    book.save
  end

こんな感じになる?

作成ではなく、まとめて更新するときは同様にAR#updateで、第一引数にはIDが入ったArray(Indexを指定しないとIDがIndex=Paramsのハッシュのキー)を追加。

Book.update(params[:book].keys, params[:book].values)

とことで!

参考サイト
http://www.pen-chan.jp/~tdiary/pen-chan/20070618.html

http://leaveanotemessagebehind.blogspot.com/2007/10/textfield-on-rails.html

http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002214