Rails offer many tools for declaring associations between your ActiveRecord models. Let’s take look at how has_many
works.
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.
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
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
has_many :friends, through: :follows
has many through
exactly the same as has_many
, but the difference is at Create Reflections. This is how has many through
create the reflection.
def create(macro, name, scope, options, ar)
reflection = reflection_class_for(macro).new(name, scope, options, ar)
options[:through] ? ThroughReflection.new(reflection) : reflection
end
We create ThroughReflection based on the created HasManyReflection
. Which is a specialized class that handles operations for the joins and join table automatically for you.
Related: You probably want has_many :through.