has_manyなassociationしているModelの一括更新する場合は、普通にform_forとfields_forとaccept_nested_attributesを使えばすんなり出来るんだけど、一括更新したいケースって必ずしもassociationしているわけでもなくて、そういう場合どうすればいいかいつも迷うので自分なりのベストプラクティスを見つけとこうと思った。
こんな感じでやってみた
一括更新したいitemsをscaffoldする。
1 2 |
$ rails g scaffold items name:string price:integer |
コマンドでitemを登録しておく。
1 2 3 4 5 |
$ rails c > Item.create(name: "pencil", price: 120) > Item.create(name: "eraser", price: 90) > Item.create(name: "sellotape", price: 110) |
edit,updateアクションで一括更新処理を実装しようと思うのでidは不要だからresourceを使う。resourcesじゃないよ。
routes.rb
1 2 |
resource :items |
viewはこんな感じ。
_form.html.erb
1 2 3 4 5 6 7 8 9 10 11 |
<%= form_tag items_path, method: :put do %> <% @items.each do |item| %> <%= fields_for "items[]", item do |fi| %> <%= fi.text_field :name %> <%= fi.text_field :price %> <br /> <% end %> <% end %> <%= submit_tag %> <% end %> |
items_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class ItemsController < ApplicationController respond_to :html def edit @items = Item.all end def update @items = items_params.map do |id, item_param| item = Item.find(id) item.update_attributes(item_param) item end respond_with(@items, location: edit_items_path) end private def items_params params.permit(items: [:name, :price])[:items] end end |
画面
ポイント
fields_forの第一引数に[]をつけると、各formのnameにitems[id]
という形式で展開される。
_form.html.erb
1 2 3 4 5 6 |
<%= fields_for "items[]", item do |fi| %> <%= fi.text_field :name %> <%= fi.text_field :price %> <br /> <% end %> |
html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<form action="/items" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓" /><input type="hidden" name="_method" value="put" /><input type="hidden" name="authenticity_token" value="4pSrlIZ8Oq21vXz0PZ2VYoyMGHdH7l1aaZErqIMH7h4aCCCYRi5FLa+G09/7Sb9qIPwmcTRbhGyGw3tFNvKbAw==" /> <input type="text" value="pencil" name="items[4][name]" id="items_4_name" /> <input type="text" value="120" name="items[4][price]" id="items_4_price" /> <br /> <input type="text" value="eraser" name="items[5][name]" id="items_5_name" /> <input type="text" value="110" name="items[5][price]" id="items_5_price" /> <br /> <input type="text" value="sellotape" name="items[6][name]" id="items_6_name" /> <input type="text" value="110" name="items[6][price]" id="items_6_price" /> <br /> <input type="submit" name="commit" value="Save changes" /> </form> |
普通にform_forと異なる形式になるのでstrong_parameterの部分を工夫する必要があります。
items_controller.rb
1 2 3 4 5 |
private def items_params params.permit(items: [:name, :price])[:items] end |
items_paramsを通すと下のようにパラメータを取得できます。id => attributes
という形式になるので扱いやすいかな。
1 2 3 4 |
=> {"4"=>{"name"=>"pencil", "price"=>"120"}, "5"=>{"name"=>"eraser", "price"=>"110"}, "6"=>{"name"=>"sellotape", "price"=>"110"}} |
おまけ
あれ?ロジックあってるのにちゃんと更新されないなーおかしいなーと思ったら、なんだstrong_parameterの指定漏れかよ…って経験はよくあるかと思います。
よくハマるようだったらapplication.rbかdevelopment.rbあたりにこれを指定しておくと、例外投げてくれるので指定漏れにすぐ気づけると思う。
1 2 |
config.action_controller.action_on_unpermitted_parameters = :raise |
参考
http://o.inchiki.jp/obbr/181

原田 敦

最新記事 by 原田 敦 (全て見る)
- Rails Engineでブログ機能追加するgemを作る - 2015年3月15日
- WEBエンジニア一人だけでサービスを作りきる方法-夫婦のための自動ごはん予定お知らせサービス「GoHaaan」制作でやったこと - 2015年3月7日
- CentOS6でMariaDBのDynamic Columnsを試してみた - 2015年2月28日