RailsConf 2022—Super Associations
In my previous post, I mentioned that Active Record uses class_eval
instead of
define_method
to generate faster association methods:
def self.belongs_to(name)
class_eval(<<~RUBY)
def #{name}
association("#{name}").reader
end
RUBY
end
But there’s more to this story. Active Record also allows you to override these
generated methods in your model and then call super
to access the generated
method:
class PullRequest < ApplicationRecord
belongs_to :repository
# Override the repository method generated by belongs_to
def repository
puts "My custom override behavior"
# Then call the generated method
super
end
end
class_eval
alone is not enough to provide that behavior:
PullRequest.new.repository
#=> no superclass method `repository' for #<PullRequest> (NoMethodError)
That’s because using class_eval
is effectively like doing this:
class PullRequest < ApplicationRecord
# Generated by class_eval
def repository
association(:repository).reader
end
# Replaces the generated method
def repository
puts "My custom override behavior"
super
end
end
We’re trying to call super
on a method we’ve completely overwritten. In order
for super
to work, the method needs to be defined in one of the class’s
ancestors.
Active Record handles this by creating a new module on the fly, including that module into your class, and then generating all the association methods inside that module instead of directly in your class.
def self.generated_association_methods
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
end
def self.belongs_to(name)
generated_association_methods.class_eval(<<~RUBY)
def #{name}
association("#{name}").reader
end
RUBY
end
Now our class will have a new ancestor:
PullRequest.ancestors
#=> [PullRequest, PullRequest::GeneratedAssociationMethods, Base, Object, Kernel, BasicObject]
And calling super
from our override works, because it finds the generated
method in this new ancestor.
PullRequest.new.repository
#=> My custom override behavior
#=> #<PullRequest>
That’s super!