11.14.07

ActiveRecord - How do those dynamic finders and attributes work?

Posted in Programming, Ruby at 8:32 pm by Robert Horvick

In my effort to understand more about ActiveRecord, and Ruby in general, I have been digging through AR line by line.

One of my first goals was to figure out how the dynamic attributes and find_* methods worked. I mean … you call a method and it’s not there and then somehow it gets created.

In the .NET world I would expect something like:

try {
    object.Invoke("Method", args);
} catch(MissingMethodException) {
    // Use the CodeDOM or Emit to generate some code and defer to the created object
}

Was that happening here? Turns out it’s not far off.

I started by creating a case I knew would fail:

class MissingMethod
end

mm = MissingMethod.new
mm.does_not_exist("argument")

# Exception: undefined method `does_not_exist' for #<MissingMethod:0x32171c4>

So how to capture that? Well - I figured ActiveRecord::Base needed to do that so I started there and found “method_missing”. Awesome :)

So I add a handler and now I’m looking like:

class MissingMethod
  def method_missing(method_id, *arguments)
    puts method_id
  end
end

mm = MissingMethod.new
mm.does_not_exist("argument")

# does_not_exist

Sweet.

Now I want to get that whole dynamic goodness thing to happen. So I set out to create a class that would take any missing method that starts with “call_*” and extract the “*” portion, see if that method exists and call it with the original arguments if it does.

So obj.call_real_method(arg) would in turn call obj.real_method(arg).

So now I’m left with a few tasks:

  1. split off the real method name
  2. figure out if it exists
  3. call it

The splitting was easy to figure out:

if match = /^call_([_a-zA-Z]\w*)$/.match(method_id.to_s)
  method = match.captures[0]

Figuring out if it existed took a little more research. I dug into the method_missing handler in ActiveRecord and fairly quickly ran into respond_to? which, after checking some docs, looked perfect.

if(respond_to?(method))

Finally I needed to pass along the call. I did not see an obvious invoke method so I checked out places in ActiveRecord that called respond_to? and noticed the pattern of calling __send__ shortly after. This looked promising. A few doc checks later and my sample was done.

class MissingMethod
  def method_missing(method_id, *arguments)
     if match = /^call_([_a-zA-Z]\w*)$/.match(method_id.to_s)
       method = match.captures[0]
       if(respond_to?(method))
         __send__ method, arguments
       else
         super
       end
     else
       super
     end
  end

  def display(arg)
    puts arg
  end
end

mm = MissingMethod.new
mm.call_display(”Hello World”)

When run prints “Hello World”

As for ActiveRecord - it is doing more than that, obviously. It is not just deferring to existing methods but rather also generating them, adding them to a hash and then calling the generated method from the hash. But this is that point of diminishing interest. I can see the goal line from here so it’s time to move on to the next topic before I get bogged down in the details instead of getting my head around the big picture.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

2 Comments »

  1. josh said,

    November 15, 2007 at 12:23 am

    Nice code spelunking. If you want to see my take on things (pre Rails 2.0) you can see it here:
    http://blog.hasmanythrough.com/2006/8/13/how-dynamic-finders-work

    I need to update that for Rails 2.0, which actually defines the finder method the first time it’s called, so the 2nd time through it doesn’t need to much about in method_missing again.

  2. Ruby, Rails, Rails Plugins, Mac OS X « exceptionz said,

    November 22, 2007 at 3:56 pm

    [...] How do those ActiveRecord dynamic finders and attributes work? [...]

Leave a Comment