昨日の続き。
rails2.3での一括更新画面の作り方 - うんたらかんたら日記
要件として、最大N個まで登録できて画面上にテキストボックスを予め表示したいというのは、
割とありがちなんじゃないでしょうか。
こんなやつ。
試行錯誤してやってみました。
まずは空のテキストボックスを表示させます
#controller def index @exchanges = Exchange.all (MAX_EXCHANGE - @exchanges.size).times { @exchanges << Exchange.new } end
うっかりviewにロジック書いてしまいそうですが
足りない分はcontrollerでnewして渡しています。
#view <h1>Listing exchanges</h1> <% form_tag(:action => "update") do %> <table> <tr> <th>Name</th> <th>Rate</th> </tr> <%- @exchanges.each_with_index do |exchange, i| -%> <%- fields_for exchange, :index => (exchange.id || "new_#{i}") do |f| -%> <%= f.error_messages %> <tr> <td><%= f.text_field :name %></td> <td><%= f.text_field :rate %></td> <td><%= link_to 'Destroy', exchange, :confirm => 'Are you sure?', :method => :delete %></td> </tr> <%- end -%> <%- end -%> </table> <p><%= submit_tag "一括更新" %></p> <% end -%>
fields_forのindexがポイントです。
post時のparam設定時にkeyとして使われる値なのですが
空のテキストボックスの場合、当然idは無いので、ここを空のままにしておくと
railsが落ちてしまいます。
そのため無理矢理 new_i を設定することで回避しています。
オレオレ規約で、気に食わないですがとりあえずこれでいきます。
ここまでは簡単。
続いて更新処理です
最初に書いたver
#controller def update begin # unavailable Hash.keep_if (ruby1.9) upd_exchanges = params[:exchange].reject { |k,v| k =~ /^new_/} new_exchanges = params[:exchange].reject { |k,v| k !~ /^new_/ || v[:name].blank? } Exchange.transaction do # update @exchanges = Exchange.update(upd_exchanges.keys, upd_exchanges.values) # create new_exchanges.each_value do |exchange| exc = Exchange.new(exchange) exc.save @exchanges << exc end (MAX_EXCHANGE - @exchanges.size).times { @exchanges << Exchange.new } raise 'upd error' if @exchanges.find{ |exc| exc.errors.count != 0 } end redirect_to(exchanges_url, :notice => 'ok') rescue flash[:notice] = "ng" render :action => 'index' end end
paramsから更新データと登録データを選り分けて処理をさせます。
これでも何とか動きますが新規登録のとこがブサイクです。
ActiveRecord拡張ver
昨日見た一括更新処理を参考に
一括登録 multiple_save を実装してみます。
#libに置いてrequire module ActiveRecord class Base class << self # Class methods def multiple_save(attributes) if attributes.is_a?(Array) idx = -1 attributes.collect { |attr| multiple_save(attr) } else object = new(attributes) object.save object end end end end end
updateとほとんど一緒です。
controllerでは呼ぶだけです。
#controller def update begin # unavailable Hash.keep_if (ruby1.9) upd_exchanges = params[:exchange].reject { |k,v| k =~ /^new_/} new_exchanges = params[:exchange].reject { |k,v| k !~ /^new_/ || v[:name].blank? } Exchange.transaction do # update @exchanges = Exchange.update(upd_exchanges.keys, upd_exchanges.values) # create @exchanges += Exchange.multiple_save(new_exchanges.values) (5 - @exchanges.size).times { @exchanges << Exchange.new } raise 'upd error' if @exchanges.find{ |exc| exc.errors.count != 0 } end redirect_to(exchanges_url, :notice => 'ok') rescue flash[:notice] = "ng" render :action => 'index' end end
最終的にはこうなりました
#controller include ExchangesHelper def update upd_exchanges, new_exchanges = devide_hash(params[:exchange]) begin Exchange.transaction do @exchanges = Exchange.update(upd_exchanges.keys, upd_exchanges.values) @exchanges += Exchange.multiple_save(new_exchanges.values) (MAX_EXCHANGE - @exchanges.size).times { @exchanges << Exchange.new } raise 'upd error' if @exchanges.find{ |exc| exc.errors.count != 0 } end redirect_to(exchanges_url, :notice => 'ok') rescue flash[:notice] = "ng" render :action => 'index' end end
#helper # devide hash to update and create # unavailable Hash.keep_if (ruby1.9) def devide_hash(params_exchange) [params_exchange.reject { |k,v| k =~ /^new_/}, params_exchange.reject { |k,v| k !~ /^new_/ || v[:name].blank? }] end
paramsの分割をhelperに置きました。
当初、paramsの分割、更新と登録処理を丸々 Helperに持っていってみましたが、
どうもDB操作をHelperでやるのにしっくりこず
この形になりました。
あとは、空配列作るとこは別メソッドに括りだせそうですが、一旦終了。
感想
昨日のupdateに出会ってなかったら相当汚くなってた感があります。
あと、テキストボックスをユーザ手動で増減させるとかは
工夫すればjsでいけますね。
やっぱ昨日も最後に書いたけど、親子のモデルを作って
accepts_nested_attributes_for 辺りで処理させる方式が一般的なんでしょうか?
でも、この場合親が無駄になっちゃうんだよなぁ。
他にいい実装方法などあれば、コメントいただけると有り難いです。