alias_method_chain in models
As people know, there's been a fair bit of back-and-forth between me, the apparent foe of alias_method_chain, and folks who feel that alias_method_chain is a perfectly reasonable API that people should not blindly hate.
There are basically two use-cases for alias_method_chain:
- Organizing internal code
- Modifying existing code
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!