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