alias_method_chain in models

As people know, there's been a fair bit of back-and-forth between me, the apparent foe of aliasmethodchain, and folks who feel that aliasmethodchain is a perfectly reasonable API that people should not blindly hate.

There are basically two use-cases for aliasmethodchain:

  1. Organizing internal code
  2. Modifying existing code

Using aliasmethodchain to organize internal code is an interesting discussion that I will hopefully continue to have into the future. Today, I want to address uses of aliasmethodchain to override methods in ActionController::Base or ActiveRecord::Base. People who do this are blindly using the technique that has been most evangelized as the solution to all their problems when Ruby comes with a perfectly good solution.

Consider this post from vaporbase, which I am decidedly not picking on. It represents a common idiom that people have been trying to use in Rails. First, the usage in a model:

class Foo < ActiveRecord::Base  
  include FooBar
end  

Second, the code implementation:

module FooBar  
  module ClassMethods
    def find_with_bar( *args ) 
      find_without_bar( *args )
      #...or whatever 
    end
  end

  def self.included(base)
    base.class_eval do
      extend ClassMethods
      class << self
        alias_method_chain :find, :bar
      end
    end
  end  
end  

This is exactly equivalent to:

module FooBar  
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def find(*args)
      super
      #...or whatever
    end
  end
end  

That's right... if you're looking to modify subclasses of ActiveRecord::Base or ActionController::Base, keep in mind that you're (gasp) in an OO language with inheritance and super.

If you want to modify all of the models in your application, create your own custom ActiveRecord::Base subclass, and inherit from that throughout your application. That's what inheritance is there for!

Another example

Another example, by a very, very smart person, goes even further overboard.

First, he started with:

class Post < ActiveRecord::Base  
  class << self
    def find_with_tags(*args)
      options = extract_options_from_args!(args)
      if tag = options.delete(:tags)
        options[:select] ||= 'posts.*'
        options[:joins]  ||= ''
        options[:joins]  << <<-END
          INNER JOIN posts_tags AS inner_posts_tags ON posts.id = inner_posts_tags.post_id
          INNER JOIN tags AS inner_tags ON inner_tags.id = inner_posts_tags.tag_id
        END
        add_to_conditions(options, tags.map { 'inner_tags.name = ?' }.join(' OR '), *tags)
      end
      find_without_tags(*(args + [options]))
    end
    alias_method_chain :find, :tags

    def find_with_query(*args)
      options = extract_options_from_args!(args)
      if query = options.delete(:query)
        if query.empty?
          add_to_conditions(options, 'false')
        else
          term = "%#{query}%" 
          add_to_conditions(options, "posts.content LIKE ? OR posts.title LIKE ?", term, term)
        end
      end
      find_without_query(*(args + [options]))
    end
    alias_method_chain :find, :query

    protected

    def add_to_conditions(options, condition, *args)
      condition = args.empty? ? condition : [condition, *args]
      if options[:conditions].nil?
        options[:conditions] = condition
      else
        options[:conditions] = sanitize_sql(options[:conditions]) + " AND (#{sanitize_sql(condition)})" 
      end
    end
  end
end  

Noticing it wasn't very DRY, he resorted to metaprogramming:

class Post < ActiveRecord::Base  
  class << self
    def handle_find_option(name, &block)
      eigenclass = class << self; self; end
      eigenclass.send :define_method, "find_with_#{name}_handled" do |*args|
        options = extract_options_from_args!(args)
        if option = options.delete(name)
          block[options, option]
        end
        send("find_without_#{name}_handled", *(args + [options]))
      end
      eigenclass.send :alias_method_chain, :find, "#{name}_handled" 
    end
  end
end

class Post < ActiveRecord::Base  
  handle_find_option(:tags) do |options, tags|
    options[:select] ||= 'posts.*'
    options[:joins]  ||= ''
    options[:joins]  << <<-END
      INNER JOIN posts_tags AS inner_posts_tags ON posts.id = inner_posts_tags.post_id
      INNER JOIN tags AS inner_tags ON inner_tags.id = inner_posts_tags.tag_id
    END
    add_to_conditions(options, tags.map { 'inner_tags.name = ?' }.join(' OR '), *tags)
  end

  handle_find_option(:query) do |options, query|
    if query.empty?
      add_to_conditions(options, 'false')
    else
      term = "%#{query}%" 
      add_to_conditions(options, "posts.content LIKE ? OR posts.title LIKE ?", term, term)
    end
  end
end  

An alternative, using super:

class Post < ActiveRecord::Base  
  class << self
    def find(*args)
      options = args.last.is_a?(Hash) ? args.last : {}
      add_tag_conditions(options)
      add_query_conditions(options)
      super
    end

    private
    def add_tag_conditions(options)
      if tag = options.delete(:tags)
        options[:select] ||= 'posts.*'
        options[:joins]  ||= ''
        options[:joins]  << <<-END
          INNER JOIN posts_tags AS inner_posts_tags ON posts.id = inner_posts_tags.post_id
          INNER JOIN tags AS inner_tags ON inner_tags.id = inner_posts_tags.tag_id
        END
        add_to_conditions(options, tags.map { 'inner_tags.name = ?' }.join(' OR '), *tags)
      end
    end

    def add_query_conditions(options)
      if query = options.delete(:query)
        if query.empty?
          add_to_conditions(options, 'false')
        else
          term = "%#{query}%" 
          add_to_conditions(options, "posts.content LIKE ? OR posts.title LIKE ?", term, term)
        end
      end      
    end

    def add_to_conditions(options, condition, *args)
      condition = args.empty? ? condition : [condition, *args]
      if options[:conditions].nil?
        options[:conditions] = condition
      else
        options[:conditions] = sanitize_sql(options[:conditions]) + " AND (#{sanitize_sql(condition)})" 
      end
    end
  end
end  

We just override find, have it modify the options as appropriate, and call super. This same technique works fine for your own applications' ActionController modifications, and in any case where the framework API involves subclassing. Folks: this is what subclassing is FOR!