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

こんな感じの一括更新(モデルの複数同時更新)画面を作ってみました。


この手のマスタメンテナンス系画面って需要ありそうなんだけど、調べてもあんまり情報ないです。
なんでよ?


参考サイト

htmlの生成部は、とても参考になりました。
Railsで表形式の一括更新 - このブログは証明できない。

作ってみた

view
<h1>Listing exchanges</h1>

<% form_tag(:action => "update") do %>
  <table>
    <tr>
      <th>Name</th>
      <th>Rate</th>
    </tr>
  <%- @exchanges.each do |exchange| -%>
    <%- fields_for exchange, :index => exchange.id do |f| -%>
      <%= f.error_messages %>
      <tr>
        <td><%= f.text_field :name %></td>
        <td><%= f.text_field :rate %></td>
      </tr>
    <%- end -%>
  <%- end -%>
  </table>
<p><%= submit_tag "一括更新" %></p>
<% end -%>


post時のhashは下記のようになります。

Parameters: {"commit"=>"一括更新", "authenticity_token"=>"E8iHdTe9b1fj50XWQBt8L4JmSfC3Qg+z8hHAMSAz5tM=", 
"exchange"=>{"1"=>{"name"=>"USD", "rate"=>"82.6655123456"}, 
             "2"=>{"name"=>"EUR", "rate"=>"117.966212345"}}}
model
class Exchange < ActiveRecord::Base
  validates_length_of :name, :maximum => 3
end

エラー時のrollbackを確認するために、validates_length_ofを入れときます。


controller(最初に書いたver)
  def update
    @exchanges = Exchange.find(params[:exchange].keys)
    err_flg = false

    begin
      Exchange.transaction do
        @exchanges.map do |exchange|
          params[:exchange].each do |k,v|
            if exchange.id.to_s == k
              err_flg = true if exchange.update_attributes(v)
              break
            end
          end
        end

        raise 'upd error' if err_flg
      end
      redirect_to(exchanges_url, :notice => 'ok')
    rescue => ex
      render :action => 'index'
    end
  end

う〜ん、これはダメそうな匂いがします。


調べてみると

ActiveRecord::Base (rails3ではActiveRecord::Relation)に
update(id, attributes)というメソッドがあるではないですか。


exampleも、まさに複数行更新用です。

  # Updates multiple records
  people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
  Person.update(people.keys, people.values)

でも、これエラーハンドリングしてませんね。
おしい。。

なんとか

これを利用できないかとソースを見てみると

module ActiveRecord #:nodoc:
  class Base
    class << self # Class methods
      def update(id, attributes)
        if id.is_a?(Array)
          idx = -1
          id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
        else
          object = find(id)
          object.update_attributes(attributes)
          object
        end
      end
 

更新後のobjectを返却しています。
ふむふむ使えそうです。

結局

controller(こうなりました)

#raiseは手抜き

  def update
    begin
      Exchange.transaction do
        @exchanges = Exchange.update(params[:exchange].keys, params[:exchange].values)
      raise 'upd error' if @exchanges.find{ |exc| exc.errors.count != 0 }
      end
      redirect_to(exchanges_url, :notice => 'ok')
    rescue
      render :action => 'index'
    end
  end


感想

複数エラーの場合は、scaffoldのエラー出力が複数出力されてしまい
見た目が汚いです。
この辺りをうまくやるには、モデルを入れ子にした構造で作ってやればなんとか出来そうです。


毎度思いますが、ちょっと変わったことやろうとすると面倒。