Why does Rails not reset the association when the id is changed?

Rails is great and most things just work easily. However, I've never been able to get a definite answer on whether one should do,

validates :parent, :presence => true

or,

validates :parent_id, :presence => true

given,

class Parent end

class Child   belongs_to :parent end

I've always thought that validating the :parent (and not the foreign key) is the *more* correct thing to do ... but I don't understand why Rails does not reset the parent association when the parent_id is changed as demonstrated here,

child = Child.find(..) child.parent_id = nil puts child.valid? # outputs false

child = Child.find(..) child.parent child.parent_id = nil puts child.valid? # outputs true!

Any thoughts?

I've always thought that validating the :parent (and not the foreign key) is the *more* correct thing to do ... but I don't understand why Rails does not reset the parent association when the parent_id is changed as demonstrated here,

Any thoughts?

yep... don't update foreign ids - update associated objects:

child = Child.find(..) child.parent = nil

one less step :wink:

I've never been able to get a definite answer on whether one should do,

validates :parent, :presence => true

yep - always validate the object - no sense validating a foreign key field, when the foreign key might not link to a row in the association table.

That's my preference anyhoo...

oops! This is rails-core list... take it over to "talk"

apologies...

This is due to the way that association proxies lazy load their targets. In the first case, if you only loaded the child record, and modified the parent_id attribute, then you never load the parent object, because you never accessed the association proxy.

In the latter case, you did access the association proxy, so the parent got loaded, but then you modified the parent_id, directly. I'd recommend that you be consistent -- if you're checking if the associated object exists, set the association to nil, instead of the id, and you shouldn't have a problem.

Hey,

In edge rails there is a mechanism for checking whether the loaded association target is "stale" - so if you do record.foo_id = x, then record.foo will load the target afresh.

I'm not sure whether it necessarily works with validation like this, but hopefully it does. [I haven't tried.]

Just to emphasise, this is new in edge - it is not in the 3-0-stable branch.

Jon

Thanks for confirming validate :parent is the preferred way.

yep... don't update foreign ids - update associated objects:

> child = Child.find(..) > child.parent = nil

one less step :wink:

Sure ... sometimes the parent_id though is passed via form params. Which works in 99% cases, however, I had one or two cases where something loaded the parent before setting parent_id and validating :).

This seems helpful. I'll take a look at edge.

Rails is great and most things just work easily. However, I've never been able to get a definite answer on whether one should do,

validates :parent, :presence => true

or,

validates :parent_id, :presence => true

Validate the object, not the foreign key. Otherwise a record with parent_id of -99, 0 or some other nonsense will still pass.

given,

class Parent end

class Child belongs_to :parent end

I've always thought that validating the :parent (and not the foreign key) is the *more* correct thing to do ... but I don't understand why Rails does not reset the parent association when the parent_id is changed as demonstrated here,

As Jon mentioned there is some code for this kind of thing in master. As for the historical reason it did that, it's trickier than it looks :slight_smile:

There can be multiple (or zero) associations for a given _id column, and a patch we tried a while back didn't handle all those potential cases. There's no deep philosophical reason it works that way, it's just historical / evolutionary artifacts sneaking up on you.

Unfortunately the Rails Guide on Active Record Validation and Callbacks says (Section 3.9): "If you want to be sure that an association is present, you’ll need to test whether the foreign key used to map the association is present, and not the associated object itself."

Maybe the guide needs to be updated.

Rainer

Yeah, I think the wording is unfortunate. Certainly you can't be sure the association is present by checking that the FK attribute is present.

Rather, this topic deserves a warning in my view. Something in the line that if you check whether the FK attribute is present then you *don't know* whether it is valid. You can decide to take the risk, that's up to you, but the reader should be warned.

A pointer to the validates_existence plugin would be nice. Also to FK constraints as the most robust solution, though they are kinda weird to explain in a generic way nowadays because then #save, #update_attributes and friends can throw exceptions for ordinary validation errors, and that doesn't fit well with standard idioms. This would deserve its own section in the guide with all practicals details and gotchas.

If you'd like to have a stab at any of these revisions please give it a go through docrails.

Unfortunately the Rails Guide on Active Record Validation and Callbacks says (Section 3.9): "If you want to be sure that an association is present, you’ll need to test whether the foreign key used to map the association is present, and not the associated object itself."

Maybe the guide needs to be updated.

Yeah, I think the wording is unfortunate. Certainly you can't be sure the association is present by checking that the FK attribute is present.

Rather, this topic deserves a warning in my view. Something in the line that if you check whether the FK attribute is present then you *don't know* whether it is valid. You can decide to take the risk, that's up to you, but the reader should be warned.

But this thread seems to suggest one should simply validate the association attribute instead. Is that not sufficient then?

If you'd like to have a stab at any of these revisions please give it a go through docrails.

Sorry, my understanding is too limited.

Rainer

I'd say validating the association attribute would be the best practice in 3.1, but it may result in an extra query to the database to fetch the associated record, if it's not loaded or if it's stale.

If users wish to avoid that overhead, they can check the FK, but should be aware that this does not guarantee the associated record actually exists.

You can't still be sure the association is valid, because the associated object is cached if previously fetched, and the FK can be changed directly:

  fxn@halmos:~/tmp/test_belongs_to ∵ cat app/models/post.rb   class Post < ActiveRecord::Base     has_many :comments   end

  fxn@halmos:~/tmp/test_belongs_to ∵ cat app/models/comment.rb   class Comment < ActiveRecord::Base     belongs_to :post

    validates :post, :presence => true   end

  fxn@halmos:~/tmp/test_belongs_to ∵ cat bypass_validation.rb   post = Post.create   comment = post.comments.create

  comment.post_id = -1   p comment.save

  comment.reload   p comment.post_id

  fxn@halmos:~/tmp/test_belongs_to ∵ rails runner bypass_validation.rb   true   -1

You're going to store the post_id in the database anyway. So if you're going to take the risk of having dangling records, in my view it's better to take the risk on the post_id rather than on the association. I believe that's what the quote from the guide tries to say.

The validates_existence plugin performs a query. That's closer to checking the association holds an existing record, but there's still subject to race conditions (say, a concurrent request deleting the associated record outside your transaction). The only way to be totally sure the association does exist is to move the check to who has the key to guarantee that, which is the database with FK constraints.

Hey,

> But this thread seems to suggest one should simply validate the > association attribute instead. Is that not sufficient then?

You can't still be sure the association is valid, because the associated object is cached if previously fetched, and the FK can be changed directly:

This does work 'properly' on edge, due to the stale-checking mechanism. I just tried it. Voila:

$ rails c Loading development environment (Rails 3.1.0.beta) ruby-1.9.2-p136 :001 > post = Post.create => #<Post id: 1> ruby-1.9.2-p136 :002 > comment = post.comments.create => #<Comment id: 1, post_id: 1> ruby-1.9.2-p136 :003 > comment.post_id = -1 => -1 ruby-1.9.2-p136 :004 > comment.save => false ruby-1.9.2-p136 :005 > comment.errors => {:post=>["can't be blank"]} ruby-1.9.2-p136 :006 > comment.post => nil

Jon