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
-
Mike Dalessio
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
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
- Tag changed from active_record, associations, belongs_to to active_record, associations, belongs_to, bugmash
-
Elad Meidar
-1 current situation is expected and concurrent with other attribute behaviors. The reason for that is that there is no actual
saveupon 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
@Elad: This isn't about saving to the DB, this is just about updating the associations in memory. Just as setting the
article_idthe first time sets thearticleassociation toarticle1, setting thearticle_idthe second time should set thearticletoarticle2.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
@Peter: mmm, do you see anywhere else it needs to be applied except for a foreign key assignment in a
:belongs_toassociation? -
Peter Jaros
I can't think of another place.
-
Joe Van Dyk
I've attached a patch that contains a failing test.
-
Elad Meidar
@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
+1 on the patch, applies cleanly on master and 2-3-stable.
-
Joe Van Dyk
- 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
@Joe: i think there's a logical error in your test.
You wrote:
client.firm_id = other_firm.idto update the existing firm, but the
Clientclass 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
+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>. -
Joe Van Dyk
Elad, good catch about the foreign key.
-
Elad Meidar
Thanks, but i think there must be a better way to do it (i use
evalfor god sake... :) ) and further more considerations are required as for the extra queries. -
Zach Brock
Any progress made on this?
-
Rizwan Reza
- Tag changed from 2-3-stable, active_record, associations, belongs_to, bugmash to 2-3-stable, active_record, associations, belongs_to
-
Jeff Dean
- 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 patchIn addition, for a
belongs_to :polymorphicassociation, you'll want all associated objects to reload whenever you change theidor thetypefield.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
- 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
Thank you for the feedback.
I'll find a good way to remove that loop and resubmit.
-
Jeff Dean
- 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 endThe 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 endCalling
belongs_towould 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 endIn 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" endThe second time you called
belongs_toit 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 endLet me know if there are any issues with this technique that might cause a problem.
Thank you.
-
Neeraj Singh
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.idJust tell user to not to assign to id directly. Just something to think about.
-
Aditya Sanghi
Automatic cleanup of spam.
-
Jonas Schneider
+1 I run into this bug frequently and it's causing headaches with before_save callbacks etc.
-
Santiago Pastorino
- Assigned user set to Aaron Patterson
-
Ryan Bigg
Automatic cleanup of spam.
-
Jeff Dean
Any progress on this?
-
Jeff Dean
I've confirmed that this is still an issue and recreated a patch that should apply cleanly.
-
Repository
- 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...
-
Andrew Assarattanakul
Still an issue with 2.3 stable
-
Jeff Dean
Here's a patch that should apply cleanly to the 2-3-stable branch.
-
Jon Leighton
- 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
- State changed from open to resolved
