Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 812
wulftone Avatar asked Aug 03 '12 20:08

wulftone


1 Answers

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.

like image 79
Yardboy Avatar answered Nov 16 '22 17:11

Yardboy