Dynamics finders in ActiveRecord using AOP.

Yesterday, I have been playing with Aquarium, an AOP framework for ruby. I decided hack a little bit. I try to refactor the dynamics finders of ActiveRecord, using an AOP approach.

First lets try to understand what are and how dynamics finders works.

This are queries are equvalent :

User.find(:first, :conditions => ["name = ?", name])
User.find_by_name(name)

User.find(:all, :conditions => ["city = ?", city])
User.find_all_by_city(city)

User.find(:all, :conditions => ["street = ? AND city IN (?)", street, cities])
User.find_all_by_street_and_city(street, cities)

And how this cool methods are generated? They are not generated, ActiveRecords use a bit of the method_missing magic to parse the method_name to find a pattern. A simplify version of the method_missing :

def method_missing(method_id, *arguments)
  if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
    # find...
  elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(method_id.to_s)
    # find_or_create...
  else
    super
  end
end

Now its time to introduce the AOP in the recipe. We create two advices, the pointcut is the method missing. This two advices perform as a chain, first the last defined.

class Base
    before :method=>:method_missing do |this, execution_point, *args|
        if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(args.fir st.to_s)
            # find_by_xxx... #do something smart and break the flow
        end
    end

    before :method=>:method_missing do |this, execution_point, *args|
        if match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/. match(args.first.to_s)
          # find_or_xxx #do something smart and break the flow
        end
    end

  # And so on, until you have a minimum common behaviour or an empty method_missing.   
end

We can extract more behaviour from the method missing, We can repeat this process until we have the minimum common behaviour or a empty method_missing. Sometimes we neer to break the chain at some point. In this cases we can uses around.

One of the benefits of this approach is that will be more easy to introduce new finder in rails. Currently to create a new finder we need to create a plugin in the vendor folder. For example we are going to create a find_like finder. We write our code in the vendor/plugins/find_like/lib/find_like.rb :

module ActiveRecord
  class Base
    class << self
      private
      alias_method :previous_method_missing, :method_missing
     def method_missing(method_id, *arguments)
        if match = /find_(all_like|like)_([_a-zA-Z]\wo*)/.match(method_id.to_s)
           # do something smart 
        else
          previous_method_missing(method_id, *arguments)
        end
      end
    end
  end
end

If we use the AOP approach we need to write, also our code in the vendor/plugins/find_like/lib :

module FinderLike
 # A custom finder finder_like_xxx
 before :types => :Base, :methods =>:method_missing do |execution_point,  *args|
   if match = /find_(all_like|like)_([_a-zA-Z]\w*)/.match(args.first.to_s)
     # Do something smart and break the flow     
   end
 end
end

I Don’t know if this is a good design in this particular case. But at least was a funny Kata.

Advertisements

2 comments so far

  1. Dean Wampler on

    I’ve always had refactoring Rails, Active Record in particular, on my mind as I’ve worked on Aquarium. I’m glad you have the same idea!

    Indeed, while a lot people don’t think that Ruby needs AOP, because it has good metaprogramming tools, I believe that support for AOP would result in a cleaner design and code base.

    The funny thing is, I’m not sure that refactoring the “find_*” method handling is the best use of AOP. You could argue that the “before advise” you have here is really just breaking up a big method_missing into smaller methods, which you could do with plain-old OOP. That’s not a bad idea, but it’s not as compelling an argument for AOP, IMHO. 😉

    Instead, I’ve considered AOP a better way to structure other examples in Rails, like WhinyNil and truly separating the Model in Rails MVC from any direct connection with the persistence engine, so that it’s much easier to test and manipulate independently.

    Also, there are roughly 180 cases of method aliasing in the Rails 1.2 stack, much of which would be cleaner and more robust if implemented as advice, instead. For example, using advice would make it easier for many different toolkits to “enhance” the same methods, like method_missing!, without stepping on each other so much.

    I’ve intended to blog about specific examples. Now that Aquarium has been released, I’ll do that.

  2. العاب on

    I am glad that I noticed this blog , precisely the right information that I was looking for! .


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: