RailsConf 2022—Looking at Your Own Reflection
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!