How has_many works

Rails offer many tools for declaring associations between your ActiveRecord models. Let‘s take look at how has_many works.

The setup

Suppose our model is User class. We have a friends association through follows association that represents people the user is following. The Follow belongs to follower and friends associations. The follows table has follower_id and friend_id fields.

class User < ApplicationRecord
  has_many :follows,
    foreign_key: :follower_id,
    inverse_of: :follower
  has_many :friends,
    through: :follows

  has_many :followers,
    through: :incoming_follows
  has_many :incoming_follows,
    class_name: "Follow",
    foreign_key: :friend_id,
    inverse_of: :friend
end

class Follow < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :friend, class_name: "User"
end

Let‘s dive into what happens when we execute:

has_many :follows,
  foreign_key: :follower_id,
  inverse_of: :follower

has_many defined in ActiveRecord::Associations. When code executed to:

has_many :follows,
  foreign_key: :follower_id,
  inverse_of: :follower

We are at:

# ActiveRecord::Associations::ClassMethods#has_many
def has_many(name, scope = nil, **options, &extension)
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
  Reflection.add_reflection self, name, reflection
end

The Builder::HasMany is an object that holds information about all valid options and dependent options for has many associations:

module ActiveRecord::Associations::Builder # :nodoc:
  class HasMany < CollectionAssociation #:nodoc:
    def self.macro
      :has_many
    end

    def self.valid_options(options)
      valid = super + [:counter_cache, :join_table, :index_errors]
      valid += [:as, :foreign_type] if options[:as]
      valid += [:through, :source, :source_type] if options[:through]
      valid
    end

    def self.valid_dependent_options
      [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
    end

    private_class_method :macro, :valid_options, :valid_dependent_options
  end
end

Let‘s see how build the reflection works:

reflection = Builder::HasMany.build(
  self,  # User
  name,  # :has_many
  scope, # nil
  options, # { foreign_key: :follower_id, inverse_of: :follower }
  &extension # nil
)

Builder::HasMany.build:

# ActiveRecord::Associations::Builder::Association.build
def self.build(model, name, scope, options, &block)
  if model.dangerous_attribute_method?(name)
    raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
                         "this will conflict with a method #{name} already defined by Active Record. " \
                         "Please choose a different association name."
  end

  reflection = create_reflection(model, name, scope, options, &block)
  define_accessors model, reflection
  define_callbacks model, reflection
  define_validations model, reflection
  reflection
end

First first check the association name we want to create :follows is in conflict with existing ID attributes:

"id", "id=", "id?", "id_before_type_cast", "id_was", "id_in_database"

Then we create a reflection for User:

reflection = create_reflection(model, name, scope, options, &block)

Which leads us at:

# ActiveRecord::Associations::Builder::
#   Association.create_reflection
def self.create_reflection(model, name, scope, options, &block)
  raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)

  validate_options(options)

  extension = define_extensions(model, name, &block)
  options[:extend] = [*options[:extend], extension] if extension

  scope = build_scope(scope)

  ActiveRecord::Reflection.create(macro, name, scope, options, model)
end

That checks if the association name we passed in is a Symbol. Then validates options we passed in. Define extensions. Build scopes that limits the association.

Create Reflection

Then we create the the reflection for User:

ActiveRecord::Reflection.create(
  :has_many,
  :follows,
  nil,
  { foreign_key: :follower_id, inverse_of: :follower },
  User
  )
=> ActiveRecord::Reflection::HasManyReflection

What is Reflection

A Reflection contains metadata about an association we specified. Reflections hierarchy:

AbstractReflection
  MacroReflection
    AggregateReflection
    AssociationReflection
      HasManyReflection
      HasOneReflection
      BelongsToReflection
      HasAndBelongsToManyReflection
  ThroughReflection
  PolymorphicReflection
  RuntimeReflection

The AssociationReflection cntains all metadata of an association like:

  • The association‘s primary key
  • The association‘s foreign key
  • The association‘s join table
  • The association‘s scope
  • The association‘s inverse association
  • The association‘s polymorphic information

That can drive out foreign key and join table when used in queries and form builder. The HasManyReflection is mostly the same as AssociationReflection but with some of the things specialized:

class HasManyReflection < AssociationReflection # :nodoc:
  def macro; :has_many; end

  def collection?; true; end

  def association_class
    if options[:through]
      Associations::HasManyThroughAssociation
    else
      Associations::HasManyAssociation
    end
  end

  def association_primary_key(klass = nil)
    primary_key(klass || self.klass)
  end
end

Then we step back to HERE:

# ActiveRecord::Associations::Builder::Association.build
def self.build(model, name, scope, options, &block)
  if model.dangerous_attribute_method?(name)
    raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
                         "this will conflict with a method #{name} already defined by Active Record. " \
                         "Please choose a different association name."
  end

  reflection = create_reflection(model, name, scope, options, &block)

  # HERE
  define_accessors model, reflection
  define_callbacks model, reflection
  define_validations model, reflection
  reflection
end

Which define methods on our User model, define callbacks, and define validations. Validations here is noop for has_many.

Then we back at HERE with the newly created reflection:

# ActiveRecord::Associations::ClassMethods#has_many
def has_many(name, scope = nil, **options, &extension)
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
  # HERE
  Reflection.add_reflection self, name, reflection
end

Then add the reflection ActiveRecord::Reflection.add_flection.

# ActiveRecord::Reflection.add_reflection
def add_reflection(ar, name, reflection)
  ar.clear_reflections_cache
  name = -name.to_s
  ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end

It first clears the reflection cache (set User.@__reflections to nil), convert the symbol (:follows in has_many :follows) to a frozen string (-"follows").

Then merge our new name and the reflection mapping:

{ "follows" => reflection }

into an internal _reflections hash on User model that looks like this:

{
  "follows" =>
    #<ActiveRecord::Reflection::HasManyReflection:
      @active_record=User,
      @name=:follows,
      @options={
        foreign_key: :follower_id,
        inverse_of: :follower
      },
      @plural_name="follows",
      @scope=nil>
}

That‘s everything happened for:

has_many :follows,
  foreign_key: :follower_id,
  inverse_of: :follower

By adding this line of code to our User model. We get all these to use:

user.follows
user.follows<<(object, ...)
user.follows.delete(object, ...)
user.follows.destroy(object, ...)
user.follows=(objects)
user.follow_ids
user.follow_ids=(ids)
user.follows.clear
user.follows.empty?
user.follows.size
user.follows.find(...)
user.follows.where(...)
user.follows.exists?(...)
user.follows.build(attributes = {}, ...)
user.follows.create(attributes = {})
user.follows.create!(attributes = {})
user.follows.reload

Next: How has many through works.