What's New in Edge Rails: Nested Model Mass Assignment

Posted by ryan
at 7:30 PM on Saturday, July 19, 2008

Looks like this feature has been pulled from Edge Rails though it’s tentatively scheduled to come back post the 2.2 release.

Nested models (nested forms by another name) describe the scenario when you want to create and modify values of nested attributes through a containing model. For instance, if you have an user model with many phone numbers:

1
2
3
4
5
6
7
8
9
class User < ActiveRecord::Base
  validates_presence_of :login
  has_many :phone_numbers
end

class PhoneNumber < ActiveRecord::Base
  validates_presence_of :area_code, :number
  belongs_to :user
end

you may want to be able to create the user and a group of phone numbers at the same time. This is what this looks like with the new mass assignment functionality of Rails keyed off of the :accessible option of the association declaration (:phone_numbers, in this case).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User < ActiveRecord::Base
  validates_presence_of :login
  has_many :phone_numbers, :accessible => true
end

ryan = User.create( {
  :login => 'ryan',
  :phone_numbers => [
    { :area_code => '919', :number => '123-4567' },
    { :area_code => '920', :number => '123-8901' }
  ]
})

ryan.phone_numbers.count #=> 2

A single hash of values being sent to User.create results in both a new user object and new associated phone numbers. Previously, you would have had to manually create your own phone_numbers= setter method on user to get this same functionality:

1
2
3
4
5
6
7
8
9
10
11
class User < ActiveRecord::Base

  ...

  def phone_numbers=(attrs_array)
    attrs_array.each do |attrs|
      phone_numbers.create(attrs)
    end
  end

end

Mass assignment now gives you this functionality for free.

This may not look like much, but it is a step in the direction of letting you use nested forms. Consider a user registration form where a user can enter their login name and their phone numbers in the same form (through the use of fields_for which will bundle nested model attributes into a single form):

1
2
3
4
5
6
7
8
<% form_for @user do |f| %>
  <%= f.text_field :login %>
  <% fields_for :phone_numbers do |pn_f| %>
    <%= pn_f.text_field :area_code %>
    <%= pn_f.text_field :number %>
  <% end %>
  <%= submit_tag %>
<% end %>

This form, when submitted to the following standard RESTful UserController, will correctly create the user and its associated phone numbers through the beauty of mass assignment.

1
2
3
4
5
6
7
8
9
10
class UserController < ApplicationController

  # Create a new user and their phone numbers with mass assignment
  def new
    @user = User.create(params[:user])
    respond_to do |wants|
      ...
    end
  end
end

Mass assignment can be used on all association types – :belongs_to, :has_one, :has_many and :has_and_belongs_to_many as long as the :accessible => true option is specified.

This is a very convenient addition to ActiveRecord, but the real zinger will come with full nested form support when you can create, update and delete these nested models directly from what is pushed down in the parameter hash of a form submission. This would allow for the functionality in this complex forms screencast with minimal hassle. What a fine day that would be.

tags: ruby, rubyonrails

Comments

Leave a response

  1. BizmarkJuly 19, 2008 @ 08:43 PM

    Nice!

    Though its worth mentioning that you can already do this with attribute_fu, even get javascript functionality for free for dynamic forms.

  2. Yan PritzkerJuly 19, 2008 @ 08:45 PM

    I can see this being useful also for constructing non-yml test fixtures.

  3. Ryan HeneiseJuly 19, 2008 @ 11:34 PM

    In our app a donor can have multiple email addresses, phone numbers, addresses, web addresses, etc., so we built a whole infrastructure with getter and setter methods. Hooray for the person who was to brilliant to live with this inconvenience and, rather than muddling through, gave us accessible => true. Hip Hip!

  4. RahsunJuly 20, 2008 @ 11:10 AM

    2 words “hellz yes”

    This is a useful tidbit, thanks for covering it and thanks to the person who committed this feature.

  5. MortenJuly 20, 2008 @ 11:12 AM

    I’d like to understand the transaction scheme behind? Why is it using create rather than build? Anyone know? Thanks.

  6. CameronJuly 20, 2008 @ 07:34 PM

    Please tell me that associated models validations will also bubble up to the parent or to some generic object where I can get at them… I’d love to be able to catch validations in associated models cleanly… This is awesome BTW, mass assignment recently saved me a few hundred lines, and this would save me a few hundred more…

  7. queJuly 21, 2008 @ 04:12 PM

    Imagine I have: Client has_one address_a, class => Address, conditions => ‘a’ Client has_one address_b, class => Address, conditions => ‘b’ and class Address looks like belongs_to country belongs_to city belongs_to region ..... so the address model consists of references to other models. Will I be able to do smth like: ryan = User.create( { :login => ‘ryan’, :address_a => { :country => { :name => ‘Zimbabwe’ }, :region => { :name => ‘East side’ } } :address_b => { :country => { ............... } }) In other words: what’s the maximum level of nesting?

  8. Felipe GiottoJuly 22, 2008 @ 06:33 PM

    Wow! This seems to be a huge improvement! I had to create this kind of “setter” in many projects! Now it’ll be much easier!

    Felipe Giotto.

  9. Kris RasmussenJuly 23, 2008 @ 04:12 AM

    I wrote a plugin and associated tests for something like this a few months ago. I wonder if this implementation is as robust with the edge cases. For example, can it handle the case where you mass assign some nested models, save, and then mass assign new nested models where some of them are the same as the original and some are not? In that case you would want to be sure that you do not delete the missing one’s until after validation has passed and that you are paying attention to ids.

  10. JONJuly 23, 2008 @ 06:15 AM

    How to use the same with daughter attr_accessors .

  11. Alex GregianinJuly 23, 2008 @ 09:26 PM

    I have only one question?

    Does it validate the nested model ?Thats the main problem when hadling multiple models in one form I guess.
  12. Glenn PowellJuly 24, 2008 @ 09:48 AM

    Has anyone successfully used this new feature? I got the Edge Rails, and then tried this out, but it doesn’t seem to do anything.

    the creation parameter names from the form are still not associated with the parent record. I.E. Parameters: {“user”=>{“login”=>”foobar”, “password”=>”barfoo”, “email”=>”foobar@foo.com”}, “commit”=>”Sign up”, “contact”=>{“first_name”=>”foo”}, “authenticity_token”=>”7f37bb0704c84b5a6f2ca3eb95c36f52501c3bfb”, “action”=>”create”, “controller”=>”users”}

    Where I would assume the “contact” should be INSIDE the “user” parameter. My Contact record is polymorphic (contactable), but I don’t see why that would be a problem… or maybe that’s the only problem.

    Anyways, if anyone else was successful, lemme know. Thanks

  13. Anthony NeumannJuly 24, 2008 @ 02:03 PM

    I have tried as well and this done not seem to be working. I get the same problem as Glenn Powell.. The child elements are not nested within the parent hash. If anyone has figured this out please let me know!

  14. Anthony NeumannJuly 24, 2008 @ 02:23 PM

    I got this to work by using

    <% for phone_number in @user.phone_numbers %> <% fields_for “userphone_numbers”, phone_number do |pn_f| %> <%= pn_f.text_field :area_code %> <%= pn_f.text_field :number %> <% end %> <% end %>

    but I had to build the phone numbers in the controller in the new action and cycle through them. I’m not really sure if this was necessary or if it should have worked a different way. But it did work!

  15. Glenn PowellJuly 25, 2008 @ 04:10 AM

    Ok, I don’t know why I’ve never seen anyone use, mention, or document this (maybe it’s Edge only), but when making a nested form (ex. above), you can call fields_for FROM the parent FormBuilder object (the “f” variable). EX.

    <% fields_for :user do |f| %> f.label … ... <% f.fields_for @user.contact do |f_contact| %> f_contact.text_field … ...

    I would think that you could have used “f.fields_for :contact do |f_contact|” instead (just using the symbol :contact, but I keep getting this error when I try that:

    @user[contact] is not allowed as an instance variable name

    I would have preferred to use the :contact symbol only because then I wouldn’t have to explicitly initialize the ”@user.contact = Contact.new” in my user_controller.new method. But, if I do in fact follow this course of action, then the fields appear to be named correctly. (“usercontact” ...etc.)

    In Edge rails, there is also an option that can be specified (options => { :index => index }) But this doesn’t seem to work for me. In face in order to get an array of nested children to have the correct field names, I have to use this:

    <% @user.contact.emails.each do |email| %> <% f.fields_for “emails[]”, email do |f_email| %> ...

    Go ahead and look into “rails/actionpack/lib/action_view/helpers/form_helper.rb” I found useful (and undocumented) features. But even though there does seem to be Array support in the FormBuilder.fields_for method, I still can’t find an elegant solution for my nested Array models.

    Until I find more…

  16. Glenn PowellJuly 25, 2008 @ 06:30 AM

    Finally, I got it working so that my controller doesn’t have to do ANYTHING other than: @user = User.new(params[:user]) and all the children are created correctly. You just must make sure the children are indicated in the “attr_accessible”. But only if that method call is used within your parent model, otherwise their assumed accessible.

    Here’s a couple helper functions I wrote to make creating these forms much easier:

    [code] def add_accessible_link(record_class, options, fields_options) record = record_class.new name = options[:name] ||= ActionController::RecordIdentifier.singular_class_name(record) update = options[:update] ||= name.pluralize partial = options[:partial] ||= name label = options[:label] ||= “Add #{name}” [/code]

    link_to_function label do |page|
        page.insert_html :bottom, update.to_sym, :partial => partial, :locals => { name.to_sym => record, :fields_options => fields_options }
      end
    end
    def fields_for_accessible(record, options = {}, &block)
      f = options[:f] ||= nil
      name = options[:name] ||= nil
    end
    if f
      if name
        f.fields_for(name, record, options, &block)
      else
        f.fields_for(record, options, &block)
      end
    else
      if name
        fields_for(name, record, options, &block)
      else
        fields_for(record, options, &block)
      end
    end
    add_accessible_link – This is to insert an “Add Model” link to your parent model which adds a new nested model child. The first param is the Record class of your child model (I.E. Email). The options param is to include:
    • :name => name of the local variable in the partial
    • :update => id of html element to insert new partial at end of
    • :partial => path to partial (I.E. ‘emails/edit’)
    • :label => link’s text label The fields_options param is simply passed into the partial, and is supposed to be then passed into the function described below as options param.
    fields_for_accessible – This is used in your child partials in place of fields_for (or to wrap it at least) The record param is the actual record being used. This must be a record and not a Symbol, only because I can’t get it to work with Symbols. (See above comment) The options params are:
    • f => the FormBuilder (typically called ‘f’) of the parent model
    • name => the field name. This is the name of the child within the parent. (I.E. “work_email”). This can also be made into an array if the child of the parent is an array of models. (I.E. “emails[]” if the Contact model had a has_many field called emails)

    Here’s a compound example: “views/users/new.html.erb”: [code] <% form_for :user, :url => users_path do |f| -%> <%= f.error_messages %> ... <%= render :partial => ‘contacts/edit’, :locals => { :contact => @user.contact, :fields_options => { :f => f } } %> [/code]

    “views/contacts/_edit.html.erb”: [code] <% fields_for_accessible(contact, fields_options ||= {}) do |f| %> ... <%= render :partial => ‘emails/edit’, :collection => (contact ? contact.emails : {}), :as => :email, :locals => { :fields_options => { :f => f, :name => “emails[]” } } %> <%= add_accessible_link Email, { :label => “Add Email”, :partial => ‘emails/edit’ }, { :f => f, :name => “emails[]” } %> [/code]

    “views/emails/_edit.html.erb”: [code] <% fields_for_accessible(email, fields_options ||= {}) do |f| %> ... [/code]

    Hope that’s easy enough, and that it helps. Lemme know.

  17. Glenn PowellJuly 25, 2008 @ 06:32 AM

    My last post messed the code up a bit, but I think you can still parse it out. :)

  18. Ian LotinskyJuly 25, 2008 @ 03:19 PM

    Can we use nested Fixtures now? (I didn’t see any modifications to fixtures.rb, so I’m assuming not.)

  19. Alejandro JuarezJuly 25, 2008 @ 06:38 PM

    This is an amazing feature.

    Thank you so much!

  20. Jose FernandezJuly 28, 2008 @ 02:17 PM

    Could we get a pastie of the above code?..

  21. Glenn PowellJuly 30, 2008 @ 09:40 AM

    Sure: http://pastie.org/244039

    I have included as much code as I think is necessary. Let me know if explanations are in order.

  22. Glenn PowellJuly 30, 2008 @ 09:42 AM

    Sure: http://pastie.org/244039

    I have included as much code as I think is necessary. Let me know if explanations are in order.

  23. BM5kSeptember 05, 2008 @ 04:39 AM

    I got this to work with simple (text) fields, but I’m having a LOT of trouble getting the same functionality with a file_field. Anyone have any suggestions?

    Specifically, when I create the model with files (using attachment_fu to handle images), everything is fine.

    If I edit a record without images, everything works fine.

    If I try to edit a record with images, my app blows up on save. Checking the DB it is overwriting all of the info in my images table with NULL.

  24. RickSeptember 05, 2008 @ 02:08 PM

    I have the same question as Alex… does this also solve nested validation issues?

  25. Jennifer MaasSeptember 13, 2008 @ 02:16 AM

    I was frozen to edge rails from about 3 weeks ago, and with this:

    belongs_to :address, :accessible => :true

    I kept on getting

    “Unknown key(s): accessible”

    I just upgraded to rails 2.1, and I’m still getting the same error. Am I using the key incorrectly or is this functionality not in 2.1 that isn’t frozen to edge?

  26. Jarin UdomSeptember 16, 2008 @ 07:42 PM

    @Jennifer Maas

    You need to check out Edge Rails from Github, freezing it with rake grabs an out of date version.

    git clone git://github.com/rails/rails.git vendor/rails

  27. Koos BeensSeptember 18, 2008 @ 11:07 AM

    For anyone trying this, the patch is temporarily taken out of edge. Discussion here: http://groups.google.com/group/rubyonrails-core/browse_thread/thread/3c61e00916c365e5

  28. Sai EmrysOctober 16, 2008 @ 04:59 PM

    “attrs_array.each { |attrs| phone_numbers.create(attrs) }”

    Ew!

    When is Rails going to natively support single-call mass inserts, a la ActiveRecord::Extensions?