During my talk at RailsConf 2022, I presented a simplified version of Active Record’s belongs_to and has_many methods. These methods involve some metaprogramming to generate instance methods for the association, and I demonstrated that using Ruby’s define_method:

def self.belongs_to(name)
  define_method(name) do
    association(name).reader
  end
end

define_method takes an argument with the name of the method you want to define, and a block containing the method body.

But generating these association methods with define_method is a bit of a lie. The real Active Record association methods aren’t generated with define_method. Instead, they use class_eval:

def self.belongs_to(name)
  class_eval(<<~RUBY)
    def #{name}
      association("#{name}").reader
    end
  RUBY
end

class_eval takes a string of valid Ruby code (in this example a heredoc), parses it, and evaluates it in the context of the current class. (If that sounds scary, you are correct. You’ve got to be rather careful with any of the eval methods—accidentally passing user input to eval would be a serious security vulnerability.)

I personally find that using class_eval makes the belongs_to method rather more difficult to read. So what gives? Why is Active Record using this ugly class_eval approach instead of define_method?

It turns out methods defined with class_eval will dispatch faster than methods defined with define_method (I’m assuming this is related to define_method’s block closure, but somebody please correct me if I am wrong about that). Let’s benchmark method dispatch with these two approaches:

class Testing
  define_method("defined_with_define_method") do
  end

  class_eval(<<~RUBY)
    def defined_with_class_eval
    end
  RUBY
end

testing = Testing.new

require "benchmark/ips"

Benchmark.ips do |bm|
  bm.report("define_method") { testing.defined_with_define_method }
  bm.report("class_eval")    { testing.defined_with_class_eval }
  bm.compare!
end

And the results:

Warming up --------------------------------------
       define_method     1.017M i/100ms
          class_eval     1.698M i/100ms
Calculating -------------------------------------
       define_method     10.495M (± 2.1%) i/s -     52.881M in   5.040862s
          class_eval     16.747M (± 2.6%) i/s -     84.902M in   5.073308s

Comparison:
          class_eval: 16746855.8 i/s
       define_method: 10495230.2 i/s - 1.60x  (± 0.00) slower

Wow! Calling the method defined with class_eval is quite a bit faster! So Active Record uses a bit of ugly code to speed up your calls to its generated association methods. That’s not so ugly after all!