anon_swe
anon_swe

Reputation: 9335

Rails: Why do I lose an association?

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 Papers 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

Answers (1)

Schwern
Schwern

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

Related Questions