11

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...!

wulftone
  • 1,628
  • 1
  • 18
  • 34
  • You are missing a `has_many :comments` in the User model. – jordanpg Aug 03 '12 at 21:07
  • oops, well the issue still stands. : ) I'll simplify the example. – wulftone Aug 03 '12 at 21:17
  • long question, but how does a new record have an existing association? – pjammer Aug 03 '12 at 21:25
  • I changed 'comments' to 'group' so the associations make more sense... so for instance, a User could write a new `Post` and choose an existing `Group` for the post to lie in, in a single transaction. – wulftone Aug 03 '12 at 21:31
  • The docs say that `accepts_nested_attributes_for` are for the parent object: "Nested attributes allow you to save attributes on associated records through the parent." I also notice that the error message indicates that Rails is expecting a has_one relationship: "assign_nested_attributes_for_one_to_one_association". – jordanpg Aug 03 '12 at 21:50
  • That makes more sense, I suppose, although it does work just fine if all of the records either exist or it's creating all new ones for the entire tree... so maybe it should be supported through a belongs_to and isn't? – wulftone Aug 03 '12 at 22:44
  • I tested it with `has_many :groups` instead of `belongs_to :group` (and modified the rest due to the ramifications), making it a Grandparent->Parent->Child relationship, and it still comes up with the same error. – wulftone Aug 03 '12 at 22:56
  • Sorry for the over-a-year-later response, but I was just cruising unanswered questions. :) – Yardboy Nov 14 '13 at 15:13

1 Answers1

4

Here's what I think is happening: You are passing in an ID for a group, indicating to ActiveRecord that the group exists. ActiveRecord is trying to find that group to update it with the other data you have in group_attributes. Since you are doing this inside the post_attributes, ActiveRecord is trying to find that group via the association between the post and the group. That is, ActiveRecord first looks for the associated group - where id = post.group_id - then from that result looks for the one with ID = 1. This might seem a little weird and clumsy for a parent relationship, as in your case, but I'm sure you can see this is useful behavior when going in the other direction, where the nested attributes represent one or more of potentially many children.

However, your post object, created from the data in post_attributes, is not yet associated with a group - post.group_id is nil. So, when ActiveRecord does that first search to get the associated group, it comes up empty. Correspondingly, it does not find a group with ID = 1 in the (empty) results. Technically, the record is there, but it's not there in terms of the association with the post.

You could prove this out by including group_id => 1 in post_attributes. I believe that if you do that, ActiveRecord will find the group by the association, then sub-select the group with ID = 1 out of the results, successfully, then update that group with the additional data in group_attributes.

Note also, that the only reason to include the nested attributes for group inside post like you are doing is if you are allowing the user to update the group name at the same time that they are creating a new post. If all you are looking to do is link the new post to the existing group, then you just need to include group_id in post_attributes and you can get rid of group_attributes.

Yardboy
  • 2,777
  • 1
  • 23
  • 29
  • This seems pretty logical to me, but this scenario is long gone. : ) I do remember that I _did_ want to update attributes on the association and the root record simultaneously (I wasn't using these exact User, Post, and Group tables, but it was a similar structure). I believe the need to do such an action was put on the back-burner for future implementation, then forgotten about. It may come back to haunt us! Thanks for your answer. I'll be sure to refer to it when I run into this again. – wulftone Nov 14 '13 at 20:37
  • Trying it again, I get new `Groups` created even if I have a `group_id` in the `Post`--although it doesn't throw an error. It just ignores the `group_id` and creates one anyway. Still doesn't seem possible to do. – wulftone Nov 14 '13 at 21:24
  • 1
    Dammit. Really thought I had that one nailed. I will take some time to play around with the scenario this weekend and see what I can come up with, it's stuck with me now. ;) – Yardboy Nov 15 '13 at 14:08