In my RailsConf 2022 talk I presented a simplified version of the Active Record Reflection and Association classes. A nice way to learn more about how these work is to inspect the objects in the development console of your own application (for fun and learning only, please, since much of this is private Rails API that is not meant to be used directly).

Reflections

The Reflection stores class-level metadata about the association. We can inspect one of these Reflection objects by calling the .reflect_on_association class method on the model where the association is defined:

class Repository < ApplicationRecord
  has_many :pull_requests
end

Repository.reflect_on_association(:pull_requests)
#<ActiveRecord::Reflection::HasManyReflection:0x000000010ab29f48
 @active_record=Repository(id: integer),
 @klass=nil,
 @name=:pull_requests,
 @options={},
 @plural_name="pull_requests",
 @scope=nil>

And for the inverse of that association:

class PullRequest < ApplicationRecord
  belongs_to :repository
end

PullRequest.reflect_on_association(:repository)
#<ActiveRecord::Reflection::BelongsToReflection:0x000000010f58e520
 @active_record=PullRequest(id: integer, repository_id: integer),
 @klass=nil,
 @name=:repository,
 @options={},
 @plural_name="repositories",
 @scope=nil>

I mentioned the @active_record and @name instance variables in my talk, but you can see there are several others as well. I wonder what they are for… :-)

Some instance variables get initialized lazily, so after calling various methods on the reflection (I mentioned a few of the methods in my talk, but there are a whole bunch more) it might look more like this:

#<ActiveRecord::Reflection::HasManyReflection:0x000000010ab29f48
 @active_record=Repository(id: integer),
 @class_name="PullRequest",
 @foreign_key="repository_id",
 @inverse_name=:repository,
 @inverse_of=
  #<ActiveRecord::Reflection::BelongsToReflection:0x000000010f58e520
   @active_record=PullRequest(id: integer, repository_id: integer),
   @class_name="Repository",
   @foreign_key="repository_id",
   @inverse_name=nil,
   @klass=Repository(id: integer),
   @name=:repository,
   @options={},
   @plural_name="repositories",
   @scope=nil>,
 @klass=PullRequest(id: integer, repository_id: integer),
 @name=:pull_requests,
 @options={},
 @plural_name="pull_requests",
 @scope=nil>

Notice how @class_name, @foreign_key, @klass, and @inverse_name are all populated with values. These values are cached so the reflection doesn’t have to calculate them again and again.

Notice also how @inverse_of is set to the BelongsToReflection that represents the association on the other side of the HasManyReflection.

Associations

The Association stores instance-level data about the association. We can inspect an Association by calling the #association instance method on a record:

repository = Repository.find(1)
repository.association(:pull_requests)
#<ActiveRecord::Associations::HasManyAssociation:0x000000010c8b8af0
 @association_ids=nil,
 @association_scope=nil,
 @disable_joins=false,
 @loaded=false,
 @owner=#<Repository:0x000000010b673890 id: 1>,
 @reflection=#<ActiveRecord::Reflection::HasManyReflection:0x000000010ab29f48>
 @replaced_or_added_targets=#<Set: {}>,
 @stale_state=nil,
 @target=[]>

I mentioned @loaded, @owner, @reflection, and @target in may talk, but again you see there are plenty that I left out.

I wonder what the association will look like after loading the pull requests…

repository.association(:pull_requests).loaded?
#=> false
repository.pull_requests.to_a
repository.association(:pull_requests).loaded?
#=> true

I encourage you to play around with these objects some more, and to see if you can find where these instance variables are used in rails, or where various methods are defined. Don’t forget to reflect on what you learned!