rails2.3での一括更新画面の作り方 その2

昨日の続き。
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 辺りで処理させる方式が一般的なんでしょうか?
でも、この場合親が無駄になっちゃうんだよなぁ。

他にいい実装方法などあれば、コメントいただけると有り難いです。