Alway set inverse of to get two way bindings

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the rails category.

Last Updated: 2021-05-15

I had the following code, which, due to Rails defaults, means both the tutor and law_discipline are mandatory.

class LawDisciplineOffering < ApplicationRecord
  belongs_to :tutor
  belongs_to :law_discipline
end

I then wrote the following caller code:

tutor = Tutor.new(...)
tutor.law_discipline_offerings.build({...})
tutor.save!

It failed, saying that tutor_id must be present — i.e. it was unable to associate the law_discipline_offering to the un-persisted tutor on the prior line that I just instantiated.

This was fixed by adding inverse_of here and also in the LawDiscipline model.

class LawDisciplineOffering < ApplicationRecord
  belongs_to :tutor, inverse_of: :law_discipline_offerings
  belongs_to :law_discipline, inverse_of: :law_discipline_offerings
end

So what is going on?

It turns out that associated objects do not point to in-memory objects by default. To illustrate:

prison = Prison.create(name: 'Bad House')
criminal = prison.criminals.create(name: 'Krazy 8')

# Without :inverse_of, an SQL query must be executed
criminal.prison == prison
# Prison Load (0.1ms) SELECT "prisons".* FROM "prisons" WHERE "prisons"."id" = 2 LIMIT 1
# => true

# With :inverse_of, no SQL query happens. It can handle in-memory objects.
criminal.prison == prison
# => true

By default,, a model's associations, as far as memory is concerned, are one-way bindings. The :inverse_of option basically gives us two-way memory bindings.

References