Add case insensitive finders by extending ActiveRecord
2009-12-06

I wanted to have an easy way of performing case insensitive searches using dynamic finders without manually constructing the query, so if I called a finder with the _ci suffix, e.g. Customer.find_by_name_ci(‘Big Kahuna Burgers’), it would result in a query with lower(name) = lower(‘Big Kahuna Burgers’) in the where clause.

I could have created a new class that inherited from ActiveRecord::Base and overridden _methodmissing there, but then I and other developers would have to remember to derive all my models from this new class instead of ActiveRecord::Base. Instead I chose to extend ActiveRecord::Base itself.

Overriding in ActiveRecord::Base

This is the most obvious way of doing it - just reopen the class and add the functionality in. It’s ok if you’re adding something small but can get unwieldy as you add more.

class ActiveRecord::Base
  # the new method has to be a class method
  # note that it refers to the default implementation as 
  # method_missing_without_case_insensitive_finders
  def self.method_missing_with_case_insensitive_finders(method_called, *args, &block)
    match = method_called.to_s.match(/^find_by_(\w+)_ci$/)
    if match && respond_to?("find_by_#{match[1]}")
      find(:all, :conditions => ["lower(#{match[1]}) = lower(?)", *args[0]])
    else
      method_missing_without_case_insensitive_finders(method_called, *args, &block)
    end
  end

  # method_missing is a class method, and this is how class methods are aliased
  class << self
    alias_method_chain(:method_missing, :case_insensitive_finders)
  end
end

In theory, it’s also possible to put the new functionality into a separate module to make things a bit cleaner. You can do it using either extend or include. However, I couldn’t make this work in production mode (but I’m including the code for the sake of completeness below).

Overriding using extend

module CaseInsensitiveFinders
  def method_missing_with_case_insensitive_finders(method_called, *args, &block)
    match = method_called.to_s.match(/^find_by_(\w+)_ci$/)
    if match && respond_to?("find_by_#{match[1]}")
      find(:all, :conditions => ["lower(#{match[1]}) = lower(?)", *args[0]])
    else
      method_missing_without_case_insensitive_finders(method_called, *args, &block)
    end
  end
end

The only trick is that you need to add your new method to ActiveRecord::Base as a class method, so you have to use extend instead of include:

class ActiveRecord::Base
  extend CaseInsensitiveFinders
  class << self
    alias_method_chain(:method_missing, :case_insensitive_finders)
  end
end

Overriding using include

It’s also possible to move _alias_methodchain into your module, in which case you add just one line to ActiveRecord::Base:

class ActiveRecord::Base
  include CaseInsensitiveFinders
end

To make this work, in your module you have to override self.included which gets called when the module is included. You also need to put your method in a sub-module, and call base.extend(ClassMethods). Also note the use of _base.classeval.

module CaseInsensitiveFinders
  module ClassMethods
    def method_missing_with_case_insensitive_finders(method_called, *args, &block)
      match = method_called.to_s.match(/^find_by_(\w+)_ci$/)
      if match && respond_to?("find_by_#{match[1]}")
        find(:all, :conditions => ["lower(#{match[1]}) = lower(?)", *args[0]])
      else
        method_missing_without_case_insensitive_finders(method_called, *args, &block)
      end
    end
  end

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

I put this code into _activerecordextensions.rb in the models folder, but it can go anywhere so long as it gets automatically loaded by Rails before you access your models.

Works on: Rails 2.3.5