Nested AttributesとNested Model Formsを使って親子オブジェクトを一括で登録/変更するには
Railsでは、親子構造のオブジェクトのパラメータを一回のリクエストで送り、親オブジェクトを通じて子オブジェクトのCRUDを同時に行わせることが簡単にできませんでした。Rails2.3では、これを簡単に行えるNested AttributesとNested Model Formsの機能が入りました。
まず、モデルクラス側では、以下のように accept_nested_attributes_for を記述します。
# 料理レシピクラス
class CookingRecipe < ActiveRecord::Base
has_many :ingredients
accepts_nested_attributes_for :ingredients # 材料
end
すると、この親モデルクラス(上記の例ではCookingRecipe)のオブジェクトには、ingredientsのためのデータを属性の一種としてハッシュで渡すことができるようになります。このハッシュのためのアクセサは関連名_attributes(この例では、ingredients_attributes)となります。たとえば、コンソールで次のように実行することができます。
> recipe = CookingRecipe.new(:name => "肉じゃが")
> # ingredients_attributes = で2つの材料データを投入
> recipe.ingredients_attributes => {"new_0" => {:name => "じゃがいも"},
"new_1" => {:name => "にんじん"}}
ハッシュでデータを投入すると、その時点で関連オブジェクトが生成されますが、まだデータベースには登録されません。
> recipe.ingredients.size => 2 > recipe.ingredients.first.new_record? => true
親オブジェクトをsaveすると、同時に子オブジェクトもsaveされます。
> recipe.save => true > recipe.ingredients.first.new_record? => false
つまり、リクエストパラメータとして次のようなハッシュが入ってくれば、以下のように記述することができます。
# こんなハッシュがparams[:cooking_recipe]に入ってくれば
# {:name => "肉じゃが",
# :ingredients_attributes =>
# {
# "new_0" => {:name => "じゃがいも"},
# "new_1" => {:name => "にんじん"}
# }
# }
recipe = CookingRecipe.save(params[:cooking_recipe]
....
また、すでにあるオブジェクトの内容を変更したいケースでは、以下のように子オブジェクトのキーを対応するIDにしてやることでできます。
# こんなハッシュがparams[:cooking_recipe]に入ってくれば子を新たに作るのではなく更新する
# {:name => "肉じゃが",
# :ingredients_attributes =>
# {
# "13" => {:name => "じゃがいも"},
# "14" => {:name => "にんじん"}
# }
# }
recipe = CookingRecipe.find(params[:id])
recipe.attributes = params[:cooking_recipe]
recipe.save
....
モデル側の機能としては、このほか、削除をハッシュで予約しておいて、親オブジェクトのセーブ時に削除を実行する機能や、子オブジェクトのハッシュのデータ内容によってはオブジェクト生成をしない(無視する)ようにする機能もあります。また、has_oneについても使えます。詳しくはActiveRecord::NestedAttributes::ClassMethodsのAPI(accepts_nested_attributes_forの記載されているページ)を読むとよいでしょう。
さて、このようなハッシュをビューのフォーム画面から簡単に送るためには、Nested Model Formsを使います。といっても、特に難しいことはなく、従来からあるフォームやフィールドを以下のように使います。
<% form_for @cooking_recipe do |recipe_form| %><%= recipe_form.label :name, 'レシピ名:' %> <%= recipe_form.text_field :name %><% recipe_form.fields_for :ingredients do |ingredient_form| %><%= ingredient_form.label :name, '材料名:' %> <%= ingredient_form.text_field :name %><% end %> <%= recipe_form.submit %> <% end %>
ポイントとしては以下の2つが挙げられます。
- fields_for で関連名を指定します。すると、そのブロック内が、関連オブジェクトの数だけ繰り返されて、入力欄が用意されます。
- この入力画面に渡すオブジェクト(ここでは@cooking_recipe)にあらかじめ入力欄に対応する子オブジェクトを用意しておいてやります。
次の例では、newアクションで登録画面に最大5つの入力欄を表示するための準備をしています。
def new
@cooking_recipe = CookingRecipe.new
5.times {@cooking_recipe.ingredients.build}
end







