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!