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 method_missing 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_method_chain 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.class_eval.
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 activerecord_extensions.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