[Rails] Nested Object Forms を使って多対多の関係をスマートに編集 このエントリをはてなブックマークに登録

2011年04月08日

もりもりもりもり / ,

はじめに

もりやまです。
先月の震災の日の記事以来となってしまいました。まずは被災されたみなさまに、心からお見舞い申し上げます。
弊社ではだにーが宮城県出身なのですが、ご家族には大事なかったようで一安心です。
昨夜も大きな余震があってまだまだ落ち着かないですが、みんなでまた再建しましょう!

さて今回は、導入されたのは Rails 2.3 なのでもう目新しくもないですが、has_many :through で多対多に関連付けされたモデルを、Nested Object Forms を使って編集する方法がようやく理解できたのでまとめてみました。

そもそも Nested Object Forms って何?

あるモデルを編集するためのフォームの中に、そのモデルと has_many 等で関連付けされた別のモデルを合わせて編集できるようにするための機能です。
これを自力でやろうとすると、

  1. 関連付けされたモデルの部分の各フィールドの名前を決めてフォームを作成
  2. コントローラでそれを受け取ったら自力でパースしてモデルを組み立て
  3. 追加なのか更新なのか削除なのかを判別して処理

といったことを全部書くと思うのですが、レールに乗ることでこれらの大部分を自動でやってくれるようになります。

サンプルアプリを Github にアップしてありますので、以下のコードはそちらを参照しながら読んでいただければと思います。

サンプルアプリの概要


ジョブと魔法の二つのモデルを管理する機能を実装してあります。
ジョブはいくつかの魔法を使えるので、それをジョブの編集フォームから設定できるようにしてあるのですが、そこで Nested Object Forms を使用しています。

モデル

ジョブモデル(Job)と魔法モデル(Magic)、それとジョブと魔法を関連付ける中間モデル(JobMagic)の 3 つを使います。

モデルのコードでキモになるのは、job.rb の accepts_nested_attributes_for の部分です。


class Job < ActiveRecord::Base
  validates :title, :presence => true, :uniqueness => true

  has_many :magics, :through => :job_magics
  has_many :job_magics, :dependent => :destroy

  ## ここ ##
  accepts_nested_attributes_for :job_magics, :allow_destroy => true,
    :reject_if => lambda{ |attrs| attrs[:magic_id].blank? }
end

ここで指定した関連モデルが Job モデルの更新リクエストに特定の形式で含まれていた場合、Job モデルの更新と合わせて追加・更新・削除されるようになります。

ビュー

Railscasts – Nested Model Form Part 2 を参考にして、動的にフィールドの追加・削除をする機能を実装してあります。

_form.html.erb

まず大枠の _form.html.erb です。

<%= form_for @job do |f| %>
  <% if @job.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@job.errors.count, "error") %> prohibited this job from being saved:</h2>

      <ul>
      <% @job.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
  <fieldset>
    <legend>Job</legend>
    <dl>
      <dt><label for="job_title">Title:</label></dt>
      <dd><%= f.text_field :title %></dd>
    </dl>
    <dl>
      <dt><label>Magics:</label></dt>
      <dd>
        <ul id="job_magics">
          <%= f.fields_for :job_magics do |job_magic| -%>  <%# <== ここ %>
            <%= render 'job_magic_fields', :f => job_magic %>
          <%- end -%>
        </ul>
        <p><%= link_to_add_fields 'Add Magic', f, :job_magics, '#job_magics' %></p>
      </dd>
    </dl>
    <%= f.submit %> or <%= link_to 'Cancel', jobs_path %>
  </fieldset>
<%- end -%>

f.fields_for の引数に、Job モデルから直接関連付けしている :job_magics を指定します
has_many :through で関連付けた :magics ではありません
ここで指定する関連モデルは accepts_nested_attributes_for で指定されている必要があります。

_job_magic_fields.html.erb

もう一つは動的に追加される _job_magic_fields.html.erb です。

<li class="fields">
  <%= f.hidden_field :id %>  <%# <== ここと %>
  <%= f.select :magic_id, options_for_select([['---', nil]] + Magic.all.map{|m| [m.name, m.id]}, f.object.magic_id) %>  <%# <== ここ %>
  <%= link_to_remove_fields 'Remove Magic', f %>
</li>

fields_for のブロックは、各ループ毎にインスタンスの ID が hidden_field で埋めこまれたかどうかを確認して、明示的に埋め込んでいなければループの最後に自動で埋め込んでくれます。
しかし、今回の場合は <ul> 直下でループしているので、自動で埋め込まれると ul > input というおかしな構造になってしまいます。
それを回避するために、<li> の中で明示的に hidden_field :id を出力するようにしています。

もう一つ、使える魔法をプルダウンで選択する部分で、既存のレコードの場合は初期値を設定しておく必要があります。
text_field 等の場合は自動で value が設定されるのですが、select の場合は 2 番目の引数で渡す options 側で初期値を設定しておく必要があります。
そのため、現在の値をどこからか取得する必要があるのですが、f.object で現在のインスタンスを参照できました。
ここでは JobMagic モデルのインスタンスを参照できるので、そこから magic_id を取得して初期値を設定しています。

コントローラ

Nested Object Forms を使うためにコントローラで特別に必要なことはありません。

まとめ

結局のところ N:N の関係を編集するのではなく、1:N の関係にある中間モデルを対象にする、というのがキモでした。

  • accepts_nested_attributes_for で指定するのは中間モデルの方
  • fields_for で指定するのは accepts_nested_attributes_for で指定したモデル = 中間モデル
  • fields_for の中でインスタンスを参照する場合は f.object

Nested Object Forms はなんとなく複雑で敬遠していたのですが、理解してしまえばとても便利でした。
ぜひ使ってみてください。

  1. メモからはじめる情報共有 DocBase 無料トライアルを開始
  2. DocBase 資料をダウンロード

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

コメントはまだありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ