2012年11月7日水曜日

Railsでリストボックスを使って親子テーブルを同時に更新するサンプル


野球やサッカーなどで、手持ちの選手から試合ごとのメンバーを決める場合のクラス図は上図のようになる。Lineupという中間クラスを通して、GameクラスとPlayerクラスが多対多の関係になっている。

こういう構造の場合に、Gameクラスにデータを追加→Playerクラスにデータを追加→Lineupクラスにデータを追加というように、順々に画面を操作していくのは面倒である。

下図のように、GameクラスとLineupクラスを一画面で同時に追加することはできないだろうか?


まずはscaffold。(実行結果は省略。)
rails g scaffold game date:date
rails g scaffold player name:string
rails g scaffold lineup game:references player:references
rake db:migrate
次に、modelにクラス間のアソシエーションを設定する。
class Game < ActiveRecord::Base
  has_many :players, :through => :lineups
  has_many :lineups
  accepts_nested_attributes_for :lineups
end

class Lineup < ActiveRecord::Base
  belongs_to :game
  belongs_to :player
end
「has_many 子クラス :through 中間クラス」と書くことで、中間クラスを通じて子クラスの属性にアクセスすることができるようなる。

つまり、下記のように中間クラスLineupを意識せずに、子クラスPlayerを扱えるようになる。
irb(main):001:0> Game.find(1).players[0].name
=> "陽岱鋼"
「accepts_nested_attributes_for 中間クラス」と書くことで、子クラスの変更を親クラスで保存できるようになる。
irb(main):002:0> Game.find(1).players << Player.find(2)
irb(main):003:0> Game.find(1).save
irb(main):004:0> Game.find(1).players.last.name
=> "今浪"
view側の変更は下記のようになる。
<%= form_for(@game) do |f| %>
  <% if @game.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@game.errors.count, "error") %> prohibited this game from being saved:</h2>

      <ul>
      <% @game.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :date %><br />
    <%= f.date_select :date, :use_month_numbers => true  %>
  </div>
  <div class="field">
    <%= f.label :lineups %><br />
    <%= f.fields_for :lineups do |lf| %>
      <%= lf.hidden_field :id %>
      <%= lf.select :player_id, Player.all.map {|player| [player.name, player.id]}, {:selected=>lf.object.player_id}, :size => Player.all.size %>
    <% end %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
「fields_for」の内側に子クラスのform要素を記述する。
これで1つのview内に複数のmodelを扱えるようになる。

「:selected=>lf.object.player_id」は、こう記述しておくと、表示しているPlayerクラスの一覧から、Lineupクラスに登録されているものが選択された状態になる。

次に、controller側の変更は下記のようになる。
  # GET /games/new
  # GET /games/new.json
  def new
    @game = Game.new
    @game.lineups.build

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @game }
    end
  end
「@game.lineups.build」の行を追加したのみ。
ここでbuildをしておかないと、view側のfields_forで指定している:lineupsが空になってしまうために、下図のようにリストボックスが表示されなくなってしまう。


model、view、controllerの修正後は、下図のようにGameクラスの画面から、子クラスPlayerの追加、修正ができるようになる。


しかしながら、現状では、せっかくリストボックスを使っているのに複数選択に対応していない。
実質的にコンボボックスと同じ機能になってしまっている。

複数選択の対応は次回の課題としたい。

以下は、今回参考にさせていただいたブログ。

has_manyな関連先をまとめてINSERTする
http://aerial.st/archive/2011/06/11/insert-has-many-relations/

has_manyな関連で、1アクションで複数のモデルを同時に保存するには?
http://d.hatena.ne.jp/zariganitosh/20080104/1199437301

[Rails]accepts_nested_attributes_forでネストした別モデルのフォーム[Ruby]
http://shasou.blogspot.jp/2011/05/railsacceptsnestedattributesforruby.html

Nested AttributesとNested Model Formsを使って親子オブジェクトを一括で登録/変更するには
http://www.everyleaf.com/tech/ror_tips/nested-attributesnested-model-forms/



0 件のコメント:

コメントを投稿