Reputation: 9335
I've noticed some surprising behavior about foreign keys and losing associations. I have the following models:
class Paper < ApplicationRecord
belongs_to :submission
has_one :author_info
end
class Submission < ApplicationRecord
has_one :paper
end
class AuthorInfo < ApplicationRecord
belongs_to :paper
end
In an rspec test, I do this:
let(:paper) { FactoryBot.create(:paper) }
and my FactoryBot factory does this:
factory :paper do
< sets fields >
after(:create) do |paper|
paper.submission = FactoryBot.create(:submission)
paper.author_info = FactoryBot.create(:author_info)
end
end
When I inspect these objects in a console, I can do paper.submission
and paper.author_info
to retrieve the models I expect. For example, paper.submission_id
is 2 and paper.submission.id
is also 2.
Question 1:
My understanding is that, when I call paper.submission
, Rails looks up all rows in the submissions table with an id of paper.submission_id
. So I'd expect this:
paper.submission_id = 1234
not to cause this, since I haven't saved to the DB:
paper.submission # nil
Why does this cause the association to get lost?
Question 2:
Say I've just created the same objects as in Question 1, but haven't modified them (so paper.id
starts off as 1). Now I do this:
paper.id = 1234
I can still do paper.author_info
and get back the original author_info
. This is surprising in light of what happened in Question 1, because paper.author_info.paper_id
is still 1! So I'd expect Rails to look up all Paper
s with with id 1. In this case, it does look like Rails is using the DB to find associations and effectively ignoring the value of foreign keys on unsaved, in-memory objects.
So when is Rails looking up associations by foreign key in the DB vs. actually making use of foreign keys on unsaved, in-memory objects?
Upvotes: 0
Views: 619
Reputation: 164639
To Question 1, we can see what's happening in rails console
.
[10] pry(main)> paper.submission_id = 1234
=> 1234
[11] pry(main)> paper.submission
Submission Load (0.6ms) SELECT "submissions".* FROM "submissions" WHERE "submissions"."id" = $1 LIMIT $2 [["id", 1234], ["LIMIT", 1]]
=> nil
Once you change the association's ID, Rails will try to look up that association. This allows you to change the association by ID without first having to go through the expensive of loading an object. If paper.submission
did not change then paper.submission.id
and paper.submission_id
would no longer be synonyms. That would get very confusing.
To Question 2...
paper.id = 1234
I can still do paper.author_info and get back the original author_info
This is because of association caching.
It works only if you've already referred to paper.author_info
. Then it is cached. Or, in your case, because FactoryBot assigned it as paper.author_info = FactoryBot.create(:author_info)
. There is no query, it's just an attribute lookup.
If you reload paper
, change paper.id
and try paper.author_info
you will get nil
. The association will not be cached and it will try to find the AuthorInfo associated with Paper 1234.
# paper.reload will not work because the ID is wrong
paper = Paper.find(original_paper_id)
paper.id = 1234
paper.author_info # nil
AuthorInfo Load (0.6ms) SELECT "author_infos".* FROM "author_infos" WHERE "author_infos"."paper_id" = $1 LIMIT $2 [["paper_id", 1234], ["LIMIT", 1]]
=> nil
Upvotes: 2