Exploring the `:inverse_of` Option on Rails Model Associations

Ryan Stenberg, Former Developer

Article Category: #Code

Posted on

Ever seen the :inverse_of option on an association and wondered what it did and why it was there?

class Criminal < ActiveRecord::Base
 belongs_to :prison, inverse_of: :criminals
end

class Prison < ActiveRecord::Base
 has_many :criminals, inverse_of: :prison
end

The first time I saw something like that, it seemed like an unnecessary distinction to make. Why is :inverse_of a thing?

Memory Optimization When Fetching Associated Records

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

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

# Without :inverse_of
criminal.prison == prison
# Prison Load (0.1ms) SELECT "prisons".* FROM "prisons" WHERE "prisons"."id" = 2 LIMIT 1
# => true

# With :inverse_of
criminal.prison == prison
# => true

When we call criminal.prison without :inverse_of on both the :belongs_to and :has_many associations, it will hit the database. With :inverse_of, if we already have that prison record in memory then criminal.prison will point to the same prison.

It's worth clarifying that this only saves you a database look-up when going from Criminal to Prison (:belongs_to direction). Here's some console output going from the other direction:

prison = Prison.last
# Prison Load (0.1ms) SELECT "prisons".* FROM "prisons" ORDER BY "prisons"."id" DESC LIMIT 1
# => #<Prison id: 3, name: "Broadmoor", created_at: "2014-10-10 20:26:38", updated_at: "2014-10-10 20:26:38">

criminal = prison.criminals.first
# Criminal Load (0.3ms) SELECT "criminals".* FROM "criminals" WHERE "criminals"."prison_id" = 3 LIMIT 1
# => #<Criminal id: 3, name: "Charles Bronson", prison_id: 3, created_at: "2014-10-10 20:26:47", updated_at: "2014-10-10 20:26:47">

prison.criminals.first == criminal
# Criminal Load (0.2ms) SELECT "criminals".* FROM "criminals" WHERE "criminals"."prison_id" = 3 LIMIT 1
# => true

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 when one of the associations is a :belongs_to. A memory optimization isn't the only thing that :inverse_of gets you. Next, we'll take a look at two use cases involving the creation of associated records.

Creating an object and its children via accepts_nested_attributes_for in a :has_many association

Say we have a form in our app where we want the user to be able to create a prison and add one or more prisoners. Here's what our model landscape might look like:

class Prison < ActiveRecord::Base
 has_many :criminals, inverse_of: :prison

 accepts_nested_attributes_for :criminals
end

class Criminal < ActiveRecord::Base
 belongs_to :prison, inverse_of: :criminals

 validates :prison, presence: true
end

Without :inverse_of on the prison's has_many :criminals, here's what would happen upon form submission:

params = { name: 'Alcatraz', criminals_attributes: [{ name: 'Al Capone' }] }
# => {:name=>gt;"Alcatraz", :criminals_attributes=>[{:name=>"Al Capone"}]}
Prison.create(params)
 (0.1ms) begin transaction
 (0.1ms) rollback transaction
# => #<Prison id: nil, name: "Alcatraz", created_at: nil, updated_at: nil>
Prison.create!(params)
 (0.1ms) begin transaction
 (0.1ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: Criminals prison can't be blank

When Rails attempts to save the criminal, the prison has not yet been committed into the database and is therefore missing an id. This causes the criminal's validate :prison, presence: true validation to fail.

In order for this to work, we need to have that :inverse_of defined as illustrated in the example, then we can create our associated records as desired:

params = { name: 'Alcatraz', criminals_attributes: [{ name: 'Al Capone' }] }
# => {:name=>"Alcatraz", :criminals_attributes=>[{:name=>"Al Capone"}]}
Prison.create(params)
 (0.1ms) begin transaction
 SQL (3.0ms) INSERT INTO "prisons" ("created_at", "name", "updated_at") VALUES (?, ?, ?) [["created_at", Mon, 13 Oct 2014 16:34:11 UTC +00:00], ["name", "Alcatraz"], ["updated_at", Mon, 13 Oct 2014 16:34:11 UTC +00:00]]
 SQL (0.2ms) INSERT INTO "criminals" ("created_at", "name", "prison_id", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Mon, 13 Oct 2014 16:34:11 UTC +00:00], ["name", "Al Capone"], ["prison_id", 4], ["updated_at", Mon, 13 Oct 2014 16:34:11 UTC +00:00]]
 (2.3ms) commit transaction
# => #<Prison id: 4, name: "Alcatraz", created_at: "2014-10-13 16:34:11", updated_at: "2014-10-13 16:34:11">

Creating associated objects across a has_many :through

Another place where you might run into issues is when you're creating an associated object on the other side of a has_many :through. Let's make another change to our example to illustrate this use case:

class Criminal < ActiveRecord::Base
 has_many :sentences, inverse_of: :criminal
 has_many :prisons, through: :sentences
end

class Sentence < ActiveRecord::Base
 belongs_to :criminal, inverse_of: :sentences
 belongs_to :prison, inverse_of: :sentences
end

class Prison < ActiveRecord::Base
 has_many :sentences, inverse_of: :prison
 has_many :criminals, through: :sentences
end

Now, our criminals can serve multiple sentences and be associated to multiple prisons. If we wanted to do something like..

prison = Prison.create(name: 'Alcatraz')
criminal = prison.criminals.build(name: 'Al Capone')
# then save our criminal record..

Without the inverses, here's what we'd get:

criminal.save
 (0.1ms) begin transaction
 SQL (0.4ms) INSERT INTO "criminals" ("created_at", "name", "prison_id", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Mon, 13 Oct 2014 12:37:51 UTC +00:00], ["name", "Al Capone"], ["prison_id", nil], ["updated_at", Mon, 13 Oct 2014 12:37:51 UTC +00:00]]
 (8.5ms) commit transaction
# => true

criminal.prisons
 Prison Load (0.2ms) SELECT "prisons".* FROM "prisons" INNER JOIN "sentences" ON "prisons"."id" = "sentences"."prison_id" WHERE "sentences"."criminal_id" = 8
# => []

criminal.sentences
 Sentence Load (0.1ms) SELECT "sentences".* FROM "sentences" WHERE "sentences"."criminal_id" = 8
# => []

Prisoner on the loose! Our prisoner should have had a sentence for our Alcatraz prison.

Without :inverse_of on the :belongs_to associations in Sentence, our criminal doesn't have any prisons because the necessary sentence wouldn't be automatically created. Rails needs to know the inverses on the Sentence model in order to automatically create it.

With inverses:

criminal.save
 (0.1ms) begin transaction
 SQL (5.9ms) INSERT INTO "criminals" ("created_at", "name", "updated_at") VALUES (?, ?, ?) [["created_at", Mon, 13 Oct 2014 12:50:17 UTC +00:00], ["name", "Al Capone"], ["updated_at", Mon, 13 Oct 2014 12:50:17 UTC +00:00]]
 SQL (0.4ms) INSERT INTO "sentences" ("created_at", "criminal_id", "duration", "prison_id", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 13 Oct 2014 12:50:17 UTC +00:00], ["criminal_id", 5], ["duration", nil], ["prison_id", 9], ["updated_at", Mon, 13 Oct 2014 12:50:17 UTC +00:00]]
 (8.3ms) commit transaction
# => true

criminal.prisons
 Prison Load (0.2ms) SELECT "prisons".* FROM "prisons" INNER JOIN "sentences" ON "prisons"."id" = "sentences"."prison_id" WHERE "sentences"."criminal_id" = 5
# => [#<Prison id: 9, name: "Alcatraz", created_at: "2014-10-13 12:40:55", updated_at: "2014-10-13 12:40:55">]

criminal.sentences
# => [#<Sentence id: 3, prison_id: 9, criminal_id: 5, duration: nil, created_at: "2014-10-13 12:50:17", updated_at: "2014-10-13 12:50:17">]

Great justice! Our prisoner is correctly sentenced with Alcatraz.

Good News

As of version 4.1, Rails will try to automatically set the :inverse_of option for you (pull request). Given our example with Criminal having a :belongs_to association with Prison, it will attempt to derive the inverse from the class name -- in this case :criminals based on the class Criminal. Obviously, this falls apart when the names do not match up. For example, when using :class_name or :foreign_key options on your associations. In that case, :inverse_of has to be explicitly set to the correct names, so it's still worth knowing how :inverse_of works.

Related Articles