Rails – 親子テーブルを一度に更新する方法

parent_and_child_model_insert上記のような一画面に基本情報(宛先、住所)と複数の明細情報(物品名、数量)がある画面で入力された情報を親子関係にある基本テーブル(発送テーブル)と明細テーブル(物品テーブル)に同時に登録したい場合が業務アプリではけっこうあると思います。

Railsではこのようなケースに簡単に対応するための便利機能が用意されています。具体的にはモデルでaccepts_nested_attributes_forという設定を、ビューでfield_forメソッドを利用します。この機能を利用すると以下の2つメリットがあるので是非活用することをオススメします。

  1. ソースコードが短くてすむ
    ⇒ 後述しますが、特にコントローラー側の記述量は激減します。
  2. 明細データへのバリデーション結果が画面にキレイに表示できる
    ⇒ 親子関係をRailsに認識させることで明細データのバリデーション結果を表示するロジックを個別に実装する必要がなくなります

以下、モデル、コントローラー、ビューの順に実装方法を載せていきます。

 

モデル

まずモデルですが、物品の発送指示に対応するShippingモデル(親)と、その発送指示の明細にあたるGoodモデル(子)があるとします。それぞれのソースは以下のようになります。

# shipping.rb
class Shipping < ActiveRecord::Base

  has_many :goods
  accepts_nested_attributes_for :goods

  attr_accessible :to_name, :address

  # 子モデルの要素にもアクセスできるようにする
  attr_accessible :goods_attributes

  validates :to_name, :presence => true
  validates :address, :presence => true
end
# good.rb
class Good < ActiveRecord::Base

  belongs_to :shipping

  attr_accessible :name, :quantity

  validates :name, :presence => true
  validates :quantity, :presence => true
end

has_many、belongs_toでモデル間の親子関係を記述するのはおなじみですね。それ以外に重要なポイントが親モデルのShippingに2つあります。 accepts_nested_attributes_for :goods という記述でGoodモデルの要素をShippingモデルの子要素として認識させます。加えて、attr_accessible :goods_attributes という記述でGoodモデルへのアクセスを許可しています。

 

コントローラー

通常、親子関係のデータを処理する場合はコントローラーがごちゃごちゃになってしまうのですが、モデルに上記のような設定をしたおかげで以下のように記述量が恐ろしく少なくなります。(newメソッドとcreateメソッドを抜粋して載せます)

  def new
    @shipping = Shipping.new

    # 明細を3行分用意する(このあたりのロジックは要件によって変えてください)
    3.times do |
      good = Good.new
      @shipping.goods << good
    end
  end

  def create
    @shipping = Shipping.new(params[:shipping])

    if @shipping.save
      redirect_to @shipping
    else
      render :action => 'new'
    end
  end

ポイントは1つだけ。 @shipping.goods << good としてShippingオブジェクトの子要素のインスタンスを設定しています。

 

ビュー

ポイントはform_forの変数からfield_forメソッドを呼び出して子要素のフィールドを表現しているところです。

<h1>発送指示</h1>

<% if @shipping.errors.any? %>
<div id="error_explanation">
  <ul>
    <% @shipping.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
    <% end %>
  </ul>
</div>
<% end %>

<%= form_for(@shipping) do |shipping_form| %>
  <%= shipping_form.label :to_name %>
  <%= shipping_form.text_field :to_name %>

  <%= shipping_form.label :address %>
  <%= shipping_form.text_field :address %>

  <table>
    <tr>
      <th>物品名</th>
      <th>数量</th>
    </tr>
    <% @shipping.goods.each do |good| %>
      <%= shipping_form.fields_for :goods, good do |good_field| %>
      <tr>
        <td><%= good_field.text_field :name %></td>
        <td><%= good_field.text_field :quantity %></td>
      </tr>
    <% end %>
  </table>

<% end %>

ビューの前半にバリデーション結果を表示するためのロジックがありますが、ここをよくみてみると、 @shippingオブジェクトだけにアクセスしています。このオブジェクトには子オブジェクトであるgoodオブジェクトのバリデーション結果も含まれているため、わざわざ@shipping.goodとして子オブジェクトのバリデーション結果を取り出さずとも済みます。これがメリットの2番目として挙げたものです。

 

環境

Rails 3.2.9

参考文献

Ruby on Rails Documentation の field_forの箇所

やっぱり公式のドキュメントは鉄板ですね。

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です