Rails ActiveRecord Associations Inverse Scope

Scopes and Associations

Scopes help create a useful public interface and reduce code duplication. Although when applied to an association, you might find yourself scratching your head.

Let’s consider a real world example.

If you want to start a Conversation by sending Message then you might have something like this:

class Conversation < ApplicationRecord
  has_many :messages

class Message < ApplicationRecord
  belongs_to :conversation

Saving message built from a new conversation should create and link them both.

message = Conversation.new.messages.build(body: 'Hello')

 => Conversation id: nil>


=> Message id: 1, body: 'Hello', conversation_id: 1>
=> Conversation id: 1>

Adding a scope

It makes sense to sort the messages chronologically. A potential solution might look something like this:

class Conversation < ApplicationRecord
  has_many :messages, -> { order(created_id: :asc) }

But here lies the problem

The build method on an association depends on the inverse being set. Adding this scope will prevent you from building a Message on a Conversation.

message = Conversation.new.messages.build(body: 'Hello')
 => nil

   (0.2ms)  BEGIN
   (0.4ms)  ROLLBACK
 => false

=> ["Conversation must exist"]

Why is our message no longer associated to the conversation? The inverse is no longer set.

Adding a scope prevented rails from setting the inverse. Rails source code states this in the ActiveRecord::Reflection module.

# ActiveRecord::Reflection module (Rails 5.2.1)

  # Checks to see if the reflection doesn't have any options that prevent
  # us from being able to guess the inverse automatically. First, the
  # <tt>inverse_of</tt> option cannot be set to false. Second, we must
  # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
  # Third, we must not have options such as <tt>:foreign_key</tt>
  # which prevent us from correctly guessing the inverse association.
  # Anything with a scope can additionally ruin our attempt at finding an
  # inverse, so we exclude reflections with scopes.
  def can_find_inverse_of_automatically?(reflection)
    reflection.options[:inverse_of] != false &&
      VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
      !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&

How to Avoid This?

I would make two changes.

First, be explicit and set the inverse on the association.

class Conversation < ApplicationRecord
  has_many :messages, -> { order(created_id: :asc) }, inverse_of: :conversation

And second, decouple scope and the association. Define a separate scope instead of tying it into :messages. Then you could chain your :ordered scope like this: messages.ordered.

class Conversation < ApplicationRecord
  has_many :messages, inverse_of: :conversation

class Message < ApplicationRecord
  scope :ordered, -> { order(created_id: :asc) }

Don’t forget to add a test making sure inverse_of is set. Also, with a code linter like RuboCop, you can enforce this by adding a Rails/InverseOf Cop.


Losing the benefits of an inverse association are avoided by setting :inverse_of and separating scope.


Did you like this article? Check out these too.


Found this useful? Know how it can be improved? Get in touch and share your thoughts at blog@hocnest.com