rails: AcitveRecord won't accepts_nested_attributes_for new associated records with nested existing records

ActiveRecord doesn’t seem to understand that, given a set of params for an existing record with nested attributes, it can create a new nested record that itself has a nested existing record. (Relations tree: Existing -> New -> Existing)

Is this a bug, or am I missing something?

Let me show you a simple example.

Here are my models:

class User < ActiveRecord::Base
  has_many :posts
  attr_accessible :name, :posts_attributes
  accepts_nested_attributes_for :posts
end

class Post < ActiveRecord::Base
  belongs_to :group
  belongs_to :user
  attr_accessible :content, :title, :group_attributes
  accepts_nested_attributes_for :group
end

class Group < ActiveRecord::Base
  has_many :posts
  attr_accessible :name
end

I’ve made one record in each table, and related them accordingly, so each table has a record in it with an id=1–this is known. Now, if I have an existing User, a new Post, and an existing Group, and try to update that record using accepts_nested_attributes_for, it doesn’t like it:

1.9.3-p125 :044 > params
{
                  :id => 1,
                :name => "Billy",
    :posts_attributes => [
        [0] {
                          :title => "Title",
                        :content => "Some magnificent content for you!",
            :group_attributes => {
                  :id => 1,
                :name => "Group 1"
            }
        }
    ]
}
1.9.3-p125 :045 > u
#<User:0x00000002f7f380> {
            :id => 1,
          :name => "Billy",
    :created_at => Fri, 03 Aug 2012 20:21:37 UTC +00:00,
    :updated_at => Fri, 03 Aug 2012 20:21:37 UTC +00:00
}
1.9.3-p125 :046 > u.update_attributes params
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
ActiveRecord::RecordNotFound: Couldn't find Group with ID=1 for Post with ID=
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/nested_attributes.rb:462:in `raise_nested_attributes_record_not_found'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/nested_attributes.rb:332:in `assign_nested_attributes_for_one_to_one_association'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/nested_attributes.rb:288:in `group_attributes='
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/attribute_assignment.rb:94:in `block in assign_attributes'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/attribute_assignment.rb:93:in `each'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/attribute_assignment.rb:93:in `assign_attributes'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/base.rb:498:in `initialize'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/reflection.rb:183:in `new'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/reflection.rb:183:in `build_association'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/associations/association.rb:233:in `build_record'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/associations/collection_association.rb:112:in `build'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/nested_attributes.rb:405:in `block in assign_nested_attributes_for_collection_association'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/nested_attributes.rb:400:in `each'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/nested_attributes.rb:400:in `assign_nested_attributes_for_collection_association'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/nested_attributes.rb:288:in `posts_attributes='
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/attribute_assignment.rb:85:in `block in assign_attributes'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/attribute_assignment.rb:78:in `each'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/attribute_assignment.rb:78:in `assign_attributes'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/persistence.rb:216:in `block in update_attributes'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/transactions.rb:295:in `block in with_transaction_returning_status'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/transactions.rb:208:in `transaction'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/transactions.rb:293:in `with_transaction_returning_status'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.7/lib/active_record/persistence.rb:215:in `update_attributes'
  from (irb):15
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.7/lib/rails/commands/console.rb:47:in `start'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.7/lib/rails/commands/console.rb:8:in `start'
  from /home/trevor/.rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.7/lib/rails/commands.rb:41:in `<top (required)>'
  from script/rails:6:in `require'
  from script/rails:6:in `<main>'1.9.3-p125 :047 > 

It thinks it can’t find a group (with a known ID), related to a new Post. It works when I remove the ID from the group_attributes (but it creates a new group record). It works when I give the posts_attributes an ID, and remove the id from the group_attributes (and again creates a new group). It also works when they all have IDs.

The relationship is working:

1.9.3-p125 :059 > p = Post.new( { group_attributes: { name: 'Testing' } } )
#<Post:0x00000004212380> {
            :id => nil,
         :title => nil,
       :content => nil,
      :group_id => nil,
       :user_id => nil,
    :created_at => nil,
    :updated_at => nil
}
1.9.3-p125 :060 > p.group
[
    [0] #<Group:0x00000004211868> {
                :id => nil,
              :name => "Testing",
        :created_at => nil,
        :updated_at => nil
    }
]

It also completely works when using posts_attributes and group_attributes during User creation, if all of the records are new.

Shouldn’t it work still in the first example? ActiveRecord should be smart enough to figure this out…!

About this issue

  • Original URL
  • State: closed
  • Created 12 years ago
  • Comments: 28 (3 by maintainers)

Most upvoted comments

i had the same issue, for a has many association you just need to this

  accepts_nested_attributes_for :posts
  def posts_attributes=(attributes)
    self.posts << attributes.map {|item| Post.find(item[:id]) } # Preferably finding posts should be scoped
    super
  end

@brayancastrop & @MatayoshiMariano method works.

For one-to-one, we need

{..., item_id: 1, item_attributes: { id: 1, ...} }

For one-to-many, we need

{..., item_ids: [1], item_attributes: [{ id: 1, ...}] }

For Rails 5.1 I used slightly modified version of @brayancastrop code:

def items_attributes=(attributes)
  self.items = attributes.values.map { |item| Item.find(item['id']) }
  super(attributes)
end

Note that we overwrite items array. Expanding it (<<) would double the associated items on every save.

I had the same problem but I solved it, not overloading the #posts_attributes= as @brayancastrop says.

    accepts_nested_attributes_for :photos #This goes in user.rb
    User.new(name: "pepe", photo_ids: [10], photos_attributes: [{ id: 10, position: 0 }])

Here is a little bit more explained

There’s a way of solving this problem. All you need to do is set the id as well as the nested attributes.

If you have

Foo (existing) -> Bar (new) -> Baz (existing)

You can set:

{
  foo: {
    id: "foo_id",
    ...
    bar_attributes: {
      ...
      baz_id: "baz_id",
      baz_attributes: {
        id: "baz_id",
        ...
      }
    }
}

And it works.

Great solutions here! This is what I ended up with (adapted from @whisk’s example):

# Allow saving questions to this test. Typically this would disallow moving
# records from one relation to another, but because we allow dragging
# questions between sections we need to manually move them first.
#
# see: https://github.com/rails/rails/issues/7256
accepts_nested_attributes_for :questions
def questions_attributes=(attributes)
  # Get IDs for any questions that already exist.
  question_ids = attributes.map { |a| a[:id] }.compact

  # Now find them all and move them to this section.
  questions << UsabilityTestSectionQuestion
    .where(usability_test_section_id: usability_test.sections)
    .find(question_ids)

  # Update them with standard `accepts_nested_attributes_for` behaviour.
  super attributes
end

In our case the relation is UsabilityTest -> UsabilityTestSection -> UsabilityTestSectionQuestion. I want to allow moving questions between sections, but not between tests (so that our users can’t steal from other accounts).

@finbarr 's suggestion worked perfectly. I’ve been trying to figure this out for a couple of days and now it seems so obvious…: You have ModelA(new) --> ModelB(existing).

If model A belongs_to model B, and B already exists, you always need to somehow specify model_b_id for ModelA. It boils down to the fact that using nested attributes (which is common with nested forms) is not smart enough to figure things out. It’s different when you use “smart” methods like ModelA.new.model_b = ModelB.take

@wulftone As I see it, the implementation of nested_attributes is to support the very simple CRUD cases. As soon as things get more complicated, I think there are better ways to build a controller than relying on nested_attributes to handle everything.