# Normal ActiveRecord call - tasty, but still a bit bland.
Sandwich.find(:all, :conditions => ['meat = ? and tastiness = ?', 'turkey', 'medium'])
# Dynamic finder - unequivocally delicious. Try it with chocolate!
Sandwich.find_all_by_meat_and_tastiness('bacon', 'very')
Sandwich.find(:all, :conditions => ['meat = ? and tastiness = ?', 'turkey', 'medium'])
# Dynamic finder - unequivocally delicious. Try it with chocolate!
Sandwich.find_all_by_meat_and_tastiness('bacon', 'very')
Fortunately, a little experimentation, a covert glance at the ActiveRecord code, and a timely discovery of the wonders of inject led me to a successful implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | class Array # Dynamic finder for Array class def method_missing(method, *args) # Get method match info; proceed only if a dynamic finder call super unless match = method.to_s.match( /^find_(by|all_by|index_by|indices_by)_([_a-zA-Z]\w*)/) # Determine finder type & attribute names/symbols based on method name finder = match.captures.first.to_sym finder_is_all = (finder == :all_by or finder == :indices_by) attr_names = match.captures.last.split('_and_') attr_symbols = attr_names.collect { |i| i.to_sym } attr_indices = (0..attr_names.size - 1) # Iterate through array elements, storing matches as we go (via 'inject') return (0..size-1).inject([]) do |matches, idx| el = self[idx] # Iterate through attribute names and match against hash values (if a hash, # attempting both string and symbol keys, since we can't distinguish them # from the dynamic method) or method names (using __send__) if el and attr_indices.all? { |attr_idx| (el.is_a?(Hash) and el[attr_symbols[attr_idx]] == args[attr_idx] || el[attr_names[attr_idx]] == args[attr_idx]) or (el.respond_to?(attr_symbols[attr_idx]) and el.__send__(attr_symbols[attr_idx]) == args[attr_idx]) } # Return first match (or index) unless this is a 'find all' command return (finder == :index_by ? idx : el) if !finder_is_all # If 'find all', add element to match array; otherwise, add index matches.push(finder == :all_by ? el : idx) else finder_is_all ? matches : nil # Return state of matches array for 'inject' purposes end end end end |
Much like ActiveRecord’s dynamic finders, this bit of code accepts find_by and find_all_by methods with any variety of attribute names (as long as they’re delimited by _and_, e.g. find_by_name_and_age). And while it doesn’t accept an options hash for miscellaneous conditioning (yet?), it does have a few extras: you can query for the indices of matching elements instead of the elements themselves (e.g., find_index_by_name or find_indices_by_name), and it will match hash key/values in addition to methods.
Some example usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # Define an array of hashes (with name, birth year, and state entries) hash_citizens = [{:name => 'Mike', :birth_year => 1982, :state => :illinois}, {:name => 'Barack', :birth_year => 1961, :state => :illinois} # Define an array of Citizen model objects (with attributes matching entries defined in hashes above) model_citizens = [Citizen.new(:name => 'Mike', :birth_year => 1982, :state => :illinois), Citizen.new(:name => 'Barack', :birth_year => 1961, :state => :illinois)] # Hash key/values will be matched against names supplied in method - :name (or 'name') for find_by_name, etc. # Any other objects will be matched using messages (method calls) hash_citizens.find_by_name('Mike') model_citizens.find_by_name('Mike') # Multiple matches will only be returned if 'all' is present in method name model_citizens.find_by_state(:illinois) # Returns the first match only model_citizens.find_all_by_state(:illinois) # Returns all matches in array, in order # Indexes are returned similarly model_citizens.find_index_by_name('Barack') # Returns 1, the position of 'Barack' in the array model_citizens.find_indices_by_state(:illinois) # Returns [0,1] # If no matches are found, returns nil (singular calls) or empty array ('all' calls) model_citizens.find_by_other_stuff # Returns nil model_citizens.find_all_by_other_stuff # Returns empty array |







