Skip to content


Subversion checkout URL

You can clone with
Download ZIP


Allow nested attributes in associations to update values in it's owner o... #3991

merged 1 commit into from

5 participants


This patch allow nested attributes on an association to update the owner object. This worked in Rails 3.0.x, but stopped working in Rails 3.1.

Here's the example between Rails 3.0 and 3.1

class Task < AR
  accepts_nested_attributes_for :project

project = Project.find 1
params[:task] = {:name => 'task', :project_attributes => {:id => '1', :name => 'new name'}}

This works in rails 3.0.x (the project name is assigned correctly), but it does not work in rails 3.1.x

project.tasks.create params[:task]
puts # 'new name'

In rails 3.1.x an exception is raised...
ActiveRecord::RecordNotFound, "Couldn't find Project with ID=1 for Task with ID="

The following works in rails 3.1.x even though we're creating the new task from the relation owned by the project object. We still need to pass the project id in explicitly so that it updates correctly.

project.tasks.create params[:task].merge(:project_id =>
puts # 'new name'

The patch allows the association to work as it did in Rails 3.0.

To fix the issue, the patch does the following...

  • Merges in the creation attributes of the owner object so that the new association has reference to the owner.
  • Defers assignment of any attributes that contain "sub-attributes" (in hashes) and makes sure that the base values (the first level of attribute keys) are assigned first as this is where the owner foreign key and other references for the new object will be set.
  • The deferred nested attributes are then assigned now that the first level of data has been set.

The solution given above for 3.1 can work, but has side affects because of possible hash ordering issues. The :project_id is merged in, but happens to be assigned before the :project_attributes nested attributes are. This of course would fail if Ruby references the :project_attributes attribute before the :project_id and the exception would again be raised.

This patch prevents this issue altogether since the nested attribute assignment is deferred until the base attributes are first set.


Well, it indeed should work, so +1, but if you are doing this in a real project, it's something wrong with the code.


This looks good to me. /cc @jonleighton


Sure, looking at the example you might think needing to do this seems a bit odd, but trust me, where I need this (in a complex form) it makes things a lot cleaner. Plus it worked fine in 3.0 and stopped working in 3.1, so lets make it work again. ;)


Hmmm.... this doesn't feel like the cleanest thing in the world. Are you sure that this worked flawlessly on 3.0? I had a glance at the code but couldn't really see how this same problem wouldn't have occurred there.

Have you tried using :inverse_of on your Project#tasks association? Really that should be the 'proper' solution so that the tasks.project inverse gets set correctly when projects.tasks.create is called, and thus your :project_attributes gets assigned to the appropriate project.

I'm not against merging this, just hesitant if there is a better solution. But if it's definitely a regression we should probably just merge (but I am still curious what changed to actually cause the regression...)


Alright, let's just merge this. If anyone wants to investigate why 3.0 is different then I'd be interested, but whatevs :)

@jonleighton jonleighton merged commit 0ddb9d6 into rails:master

The "owner values" were being set differently in 3.0, so the addition of merging creation attributes was one part of the fix. There was a fair amount of refactoring done here between 3.0 and 3.1 in the ActiveRecord API.

The issue of nested attributes being assigned before the base attributes were set didn't come up for some reason in 3.0, but it was an issue in 3.1, so making sure base attributes were assigned first was the final step in getting things to work. The hash "order" seemed to be "good enough" in 3.0, but not in 3.1.


Also as Jose says, the attribute assignment is much more deterministic now... no guess work on "hash order".


Do you think there's another way of fixing that? The code doesn't work with mass assignment protection turned on. I had to remove it on master. Check the pull request for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 15, 2011
  1. @akaspick

    Allow nested attributes in associations to update values in it's owne…

    akaspick committed
    …r object. Fixes a regression from 3.0.x
This page is out of date. Refresh to see the latest.
2  activerecord/lib/active_record/associations/association.rb
@@ -230,6 +230,8 @@ def association_class
def build_record(attributes, options)
+ attributes = (attributes || {}).reverse_merge(creation_attributes)
reflection.build_association(attributes, options) do |record|
12 activerecord/lib/active_record/base.rb
@@ -1776,6 +1776,7 @@ def assign_attributes(new_attributes, options = {})
attributes = new_attributes.stringify_keys
multi_parameter_attributes = []
+ nested_parameter_attributes = []
@mass_assignment_options = options
unless options[:without_protection]
@@ -1786,12 +1787,21 @@ def assign_attributes(new_attributes, options = {})
if k.include?("(")
multi_parameter_attributes << [ k, v ]
elsif respond_to?("#{k}=")
- send("#{k}=", v)
+ if v.is_a?(Hash)
+ nested_parameter_attributes << [ k, v ]
+ else
+ send("#{k}=", v)
+ end
raise(UnknownAttributeError, "unknown attribute: #{k}")
+ # assign any deferred nested attributes after the base attributes have been set
+ nested_parameter_attributes.each do |k,v|
+ send("#{k}=", v)
+ end
@mass_assignment_options = nil
5 activerecord/test/cases/nested_attributes_test.rb
@@ -617,6 +617,11 @@ def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_
assert_equal ['Grace OMalley', 'Privateers Greed'], [,]
+ def test_should_take_a_hash_with_owner_attributes_and_assign_the_attributes_to_the_associated_model
+ @pirate.birds.create :name => 'bird', :pirate_attributes => {:id =>, :catchphrase => 'Holla!'}
+ assert_equal 'Holla!', @pirate.reload.catchphrase
+ end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{} with ID=1234567890 for Pirate with ID=#{}" do
@pirate.attributes = { association_getter => [{ :id => 1234567890 }] }
5 activerecord/test/models/bird.rb
@@ -1,9 +1,12 @@
class Bird < ActiveRecord::Base
+ belongs_to :pirate
validates_presence_of :name
+ accepts_nested_attributes_for :pirate
attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
Something went wrong with that request. Please try again.