Kesava Mallela

Rails Developer and an Interaction Designer - San Francisco Bay Area.

Dynamic Finders in ActiveRecord

This is the first of series of posts of my exploration of Rails source code.

ActiveRecord in Rails provides very convenient dynamic attribute-based finders. This can be very clean way of getting objects without writing queries in SQL. So for example, if your User model has first_name and last_name attributes, this produces finder methods like User.find_by_first_name, User.find_by_last_name or even User.find_all_by_first_name_and_last_name.

The way ActiveRecord implements these dynamic finders is by using some metaprogramming magic. Ruby provides a Kernel method called method_missing. When you send a message to an object, Ruby looks for the method with the same name as the message. It starts by looking in the current self object’s own instance methods. It then looks in its class definition and all the modules included in that class. It then follows the lookup in all the super classes and the modules included in them. If it still fails to find the method by the same name, it invokes a Kernel method called method_missing.

ActiveRecord uses method_missing to define find_by methods. But since, method_missing is basically a last resort method, this can be a serious performance bottleneck. What I discovered is that ActiveRecord does some further awesome metaprogramming by defining the new finder method as a class method !! Thus, any further calls to the same finder method would not hit the method_missing because it is now a class method.

ActiveRecord defines dynamic finders as class methods - activerecord/lib/active_record/base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def method_missing(method_id, *arguments, &block)
         if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
           attribute_names = match.attribute_names

        ...
      
              if match.respond_to?(:scope?) && match.scope?
             self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
               def self.#{method_id}(*args)                                    # def self.scoped_by_user_name_and_password(*args)
                 attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] #   attributes = Hash[[:user_name, :password].zip(args)]
                                                                               #
                 scoped(:conditions => attributes)                             #   scoped(:conditions => attributes)
               end                                                             # end
             METHOD
             send(method_id, *arguments)
          ...


              end
      end
end

Here above in the code snippet, after DynamicFinderMatch asserts that it is infact a “find_by” prefix for the method, it declares the method as a class method and then sends it to self. I am going to use this awesome technique in my work!

Comments