This project is archived and is in readonly mode.
Setting the id of a belongs_to object does not update the reference to the object
Reported by Jeff Dean | August 3rd, 2009 @ 07:54 PM | in 3.1
Sorry for the confusing title. Here's the most concise explanation:
require 'test_helper'
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :article
end
class CommentTest < ActiveSupport::TestCase
test "assigning an associated object id replaces the reference to the associated object" do
article1 = Article.create! :title => "First"
article2 = Article.create! :title => "Second"
comment = Comment.new
comment.article_id = article1.id
assert_equal article1, comment.article
comment.article_id = article2.id
assert_equal article2, comment.article # FAILS!
end
end
This is with the following schema, on a brand new edge rails project with no other plugins or gems installed:
ActiveRecord::Schema.define(:version => 20090803184410) do
create_table "articles", :force => true do |t|
t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "comments", :force => true do |t|
t.integer "article_id"
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
end
end
Comments and changes to this ticket
-
Mike Dalessio August 3rd, 2009 @ 08:53 PM
This seems to be unexpected behavior to me. When updating association ids via form parameters, the associated object can't be used in, e.g., before_save callbacks.
+1
-
Adam Milligan August 3rd, 2009 @ 09:28 PM
I run into this bug about once a month, and it drives me up the wall. I patched it at one point, which created an infinite loop at validation time due to the fact that ActiveRecords now implicitly validates_associated their associations by default.
A valid fix is a pain in the ass and a half that I haven't inflicted on myself yet.
-
CancelProfileIsBroken September 25th, 2009 @ 01:00 PM
- Tag changed from active_record, associations, belongs_to to active_record, associations, belongs_to, bugmash
-
Elad Meidar September 25th, 2009 @ 11:04 PM
-1 current situation is expected and concurrent with other attribute behaviors. The reason for that is that there is no actual
save
upon the new association id assignment, i find it very confusing if there were some attributes (association ids for ex.) that will be saved immediately upon change, and some aren't, current state where you have tosave
/ update_attributeis what i would expect.
-
Peter Jaros September 26th, 2009 @ 02:23 AM
@Elad: This isn't about saving to the DB, this is just about updating the associations in memory. Just as setting the
article_id
the first time sets thearticle
association toarticle1
, setting thearticle_id
the second time should set thearticle
toarticle2
.This isn't expected to be reflected in the database until save, but I do expect it to happen in memory, and I get quite confused when it doesn't. There should never be a time when
comment.article.id != comment.article_id
. -
Elad Meidar September 26th, 2009 @ 02:36 AM
@Peter: mmm, do you see anywhere else it needs to be applied except for a foreign key assignment in a
:belongs_to
association? -
Elad Meidar September 27th, 2009 @ 01:25 AM
@Joe: Master? 2-3-stable?
The way i see it, is that the way to solve it is to create a custom getter for any foreign key on a :belongs_to model..(child) and reload the parent association after assignment.
any other idea?
-
Elad Meidar September 27th, 2009 @ 01:30 AM
+1 on the patch, applies cleanly on master and 2-3-stable.
-
Joe Van Dyk September 27th, 2009 @ 01:34 AM
- Tag changed from active_record, associations, belongs_to, bugmash to 2-3-stable, active_record, associations, belongs_to, bugmash
2.3 stable, sorry.
Elad, that was my thinking. Also, the association should only be reloaded if the foreign key was changed.
-
Elad Meidar September 27th, 2009 @ 02:21 AM
@Joe: i think there's a logical error in your test.
You wrote:
client.firm_id = other_firm.id
to update the existing firm, but the
Client
class indicates:class Client < Company belongs_to :firm, :foreign_key => "client_of"
so i am changing it. to
client.client_of = other_firm.id
-
Elad Meidar September 27th, 2009 @ 03:03 AM
+1 verified the test fails on 2-3-stable, iv'e attached a patch that reloads all the associations that rely on a specific column, once it's changed. THERE ARE FAILING TESTS and i kept them for a reason, it seems to put on some extra overhead in terms of extra SQL queries.
As you can see, all failing tests are tests that assert a certain number of queries to be preformed. note that the actual overload is about 300%.
I think that this requires some more discussion, or at least a better way to let the developer decide.
1) Failure: test_create_without_loading_association(HasManyAssociationsTest) [./test/cases/../../lib/active_record/test_case.rb:31:in `assert_queries' ./test/cases/associations/has_many_associations_test.rb:456:in `test_create_without_loading_association' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run']: 7 instead of 1 queries were executed. Queries: SELECT * FROM `companies` WHERE (`companies`.`id` = 1 AND (1 = 1)) AND ( (`companies`.`type` = 'Firm' ) ) SELECT * FROM `companies` WHERE (`companies`.`id` = 1 AND (1 = 1)) AND ( (`companies`.`type` = 'Firm' ) ) SELECT * FROM `companies` WHERE (`companies`.`id` = 1) AND ( (`companies`.`type` = 'Firm' ) ) SELECT * FROM `companies` WHERE (`companies`.`id` = 1) AND ( (`companies`.`type` = 'Firm' ) ) SELECT * FROM `companies` WHERE (`companies`.`id` = 1) AND ( (`companies`.`type` = 'Firm' ) ) SELECT * FROM `companies` WHERE (`companies`.`id` = 1) AND ( (`companies`.`type` = 'Firm' ) ) INSERT INTO `companies` (`name`, `client_of`, `rating`, `firm_name`, `ruby_type`, `type`, `firm_id`) VALUES('Superstars', 1, 1, NULL, NULL, 'Client', NULL). <1> expected but was <7>. 2) Failure: test_associate_existing(HasManyThroughAssociationsTest) [./test/cases/../../lib/active_record/test_case.rb:31:in `assert_queries' ./test/cases/associations/has_many_through_associations_test.rb:27:in `test_associate_existing' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run']: 5 instead of 1 queries were executed. Queries: SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `people` WHERE (`people`.`id` = 2) SELECT * FROM `people` WHERE (`people`.`id` = 2) INSERT INTO `readers` (`post_id`, `person_id`) VALUES(2, 2). <1> expected but was <5>. 3) Failure: test_associate_new_by_building(HasManyThroughAssociationsTest) [./test/cases/../../lib/active_record/test_case.rb:31:in `assert_queries' ./test/cases/associations/has_many_through_associations_test.rb:76:in `test_associate_new_by_building' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run']: 13 instead of 5 queries were executed. Queries: UPDATE `posts` SET `body` = 'Like I hopefully always am-changed' WHERE `id` = 2 INSERT INTO `people` (`primary_contact_id`, `lock_version`, `gender`, `first_name`) VALUES(NULL, 0, NULL, 'Bob') SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `people` WHERE (`people`.`id` = 7) SELECT * FROM `people` WHERE (`people`.`id` = 7) INSERT INTO `readers` (`post_id`, `person_id`) VALUES(2, 7) INSERT INTO `people` (`primary_contact_id`, `lock_version`, `gender`, `first_name`) VALUES(NULL, 0, NULL, 'Ted') SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `people` WHERE (`people`.`id` = 8) SELECT * FROM `people` WHERE (`people`.`id` = 8) INSERT INTO `readers` (`post_id`, `person_id`) VALUES(2, 8). <5> expected but was <13>. 4) Failure: test_associate_with_create(HasManyThroughAssociationsTest) [./test/cases/../../lib/active_record/test_case.rb:31:in `assert_queries' ./test/cases/associations/has_many_through_associations_test.rb:140:in `test_associate_with_create' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run']: 6 instead of 2 queries were executed. Queries: INSERT INTO `people` (`primary_contact_id`, `lock_version`, `gender`, `first_name`) VALUES(NULL, 0, NULL, 'Jeb') SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `people` WHERE (`people`.`id` = 9) SELECT * FROM `people` WHERE (`people`.`id` = 9) INSERT INTO `readers` (`post_id`, `person_id`) VALUES(2, 9). <2> expected but was <6>. 5) Failure: test_associating_new(HasManyThroughAssociationsTest) [./test/cases/../../lib/active_record/test_case.rb:31:in `assert_queries' ./test/cases/associations/has_many_through_associations_test.rb:48:in `test_associating_new' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__' ./test/cases/../../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run']: 6 instead of 2 queries were executed. Queries: INSERT INTO `people` (`primary_contact_id`, `lock_version`, `gender`, `first_name`) VALUES(NULL, 0, NULL, 'bob') SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `posts` WHERE (`posts`.`id` = 2) SELECT * FROM `people` WHERE (`people`.`id` = 12) SELECT * FROM `people` WHERE (`people`.`id` = 12) INSERT INTO `readers` (`post_id`, `person_id`) VALUES(2, 12). <2> expected but was <6>.
-
Elad Meidar September 27th, 2009 @ 03:43 AM
Thanks, but i think there must be a better way to do it (i use
eval
for god sake... :) ) and further more considerations are required as for the extra queries. -
Rizwan Reza February 12th, 2010 @ 12:46 PM
- Tag changed from 2-3-stable, active_record, associations, belongs_to, bugmash to 2-3-stable, active_record, associations, belongs_to
-
Jeff Dean June 19th, 2010 @ 07:38 AM
- Tag changed from 2-3-stable, active_record, associations, belongs_to to active_record, associations, belongs_to, patch
This issue is far more complex than I realized, but luckily the fix was simple and clean. Take this example:
class Client < Company belongs_to :firm, :foreign_key => "client_of" belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1] belongs_to :sponsor, :polymorphic => true end firm1 = Firm.create client = Client.new :client_of => firm1.id client.firm == client.firm_with_other_name == client.firm_with_condition == firm1 # => true firm2 = Firm.create client.client_of => firm2.id client.firm == client.firm_with_other_name == client.firm_with_condition == firm2 # => false on master, true in this patch member = Member.create client.sponsor_id = member.id client.sponsor_type = "Member" client.sponsor == member # => true client.sponsor_type = "Firm" client.sponsor == nil # => false on master, true in this patch
In addition, for a
belongs_to :polymorphic
association, you'll want all associated objects to reload whenever you change theid
or thetype
field.To make this work, every time the foreign key changes, we have to loop through the belongs_to associations and make the appropriate objects nil.
The attached patch makes this work, and I've included test coverage for both regular and polymorphic belongs_to associations, and this patch does not break any tests on master.
-
Michael Koziarski June 23rd, 2010 @ 05:19 AM
- Milestone set to 3.1
Looping through all associations just to call a setter seems rather expensive, I'd like to defer this one till after 3.0 ships.
However yes, this is a problem and this fix appears to be the right way to do it.
-
Jeff Dean June 23rd, 2010 @ 06:13 AM
Thank you for the feedback.
I'll find a good way to remove that loop and resubmit.
-
Jeff Dean July 4th, 2010 @ 05:08 AM
- Importance changed from to
Here's a patch that moves the loop from the setter method. The code looks like this:
def association_foreign_key_setter_method(reflection) setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection| if belongs_to_reflection.primary_key_name == reflection.primary_key_name "association_instance_set(:#{belongs_to_reflection.name}, nil);" end end.compact.join class_eval <<-FILE, __FILE__, __LINE__ 1 def #{reflection.primary_key_name}=(new_id) write_attribute :#{reflection.primary_key_name}, new_id if #{reflection.primary_key_name}_changed? #{ setters } end end FILE end
The idea is to rewrite the setter method every time you define a new belongs_to. Consider the following:
class Comment < ActiveRecord::Base belongs_to :user end
Calling
belongs_to
would create a method like this:def user_id=(new_id) write_attribute :user_id, new_id if user_id_changed? association_instance_set(:user, nil); end end
In the case that 2 associations point to
user_id
:class Comment < ActiveRecord::Base belongs_to :user belongs_to :creator, :class_name => "User", :foreign_key => "user_id" end
The second time you called
belongs_to
it would rewrite theuser_id=
method to this:def user_id=(new_id) write_attribute :user_id, new_id if user_id_changed? association_instance_set(:user, nil) association_instance_set(:creator, nil) end end
Let me know if there are any issues with this technique that might cause a problem.
Thank you.
-
Neeraj Singh July 7th, 2010 @ 06:44 PM
The code looks good to me.
Given the complexity of the code I am wondering if this problem is worth solving. How about blowing up if someone uses
client.sponsor_id = member.id
Just tell user to not to assign to id directly. Just something to think about.
-
Jonas Schneider October 10th, 2010 @ 10:08 AM
+1 I run into this bug frequently and it's causing headaches with before_save callbacks etc.
-
Santiago Pastorino October 10th, 2010 @ 05:33 PM
- Assigned user set to Aaron Patterson
-
Jeff Dean December 4th, 2010 @ 06:25 AM
I've confirmed that this is still an issue and recreated a patch that should apply cleanly.
-
Repository December 8th, 2010 @ 11:40 PM
- State changed from new to resolved
(from [7ecee054a322e214e4f285b1a8327751bd79a418]) Setting the id of a belongs_to object updates all referenced objects [#2989 state:resolved] https://github.com/rails/rails/commit/7ecee054a322e214e4f285b1a8327...
-
Jeff Dean December 11th, 2010 @ 08:44 PM
Here's a patch that should apply cleanly to the 2-3-stable branch.
-
Jon Leighton December 23rd, 2010 @ 01:41 PM
- State changed from resolved to open
This bug was actually originally reported at #142. A fix similar to the above checked in, but then reverted on the basis that there were problems with it. I've tried to take this forward by addressing the problems which were talked about in #142:
- Instead of overwriting the foreign key accessor, check its value against the id column on the target when the association is next accessed. (E.g. lazily check whether an association is stale when accessing it.)
- Building on this, I added tests and an implementation which extends this patch to support :through associations which go through a belongs_to which has its foreign key changed.
Patch is attached.
-
Jon Leighton December 23rd, 2010 @ 11:55 PM
- State changed from open to resolved
Create your profile
Help contribute to this project by taking a few moments to create your personal profile. Create your profile »
<h2 style="font-size: 14px">Tickets have moved to Github</h2>
The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>
People watching this ticket
Attachments
Referenced by
- 2989 Setting the id of a belongs_to object does not update the reference to the object (from [7ecee054a322e214e4f285b1a8327751bd79a418]) Setting...
- 142 belongs_to association not updated when assigning to foreign key If anyone is interested, this has been re-reported at #29...