# Filtering (search) a model with multiple params in hash

This function in my Profile model receive a hash from a search form and should filter the table depending on those values. I guess there is something like a pattern for such a standard behavior, right now I ended up with this (unfortunately I had to reproduce the code here, so it might not work for some reasons, but you get the idea).

Just a note: we're storing in the model some strings containing multiple data, each of these fields should be a one-to-many relationship (db normality violation). I give an example: country_preferences is a string filled up by an html select :multiple => true. In the string I then find values like: ["Australia", "China", "United Kingdom"].

  def search opts # This default doesn't work the way I would like = {:user_type => :family}

#TODO: default opts value in method sign if possible
opts[:user_type] ||= :family

# initial criteria
fields= "user_type= ?"
values= [opts[:user_type]]

# each field filters the list in one of these three ways
staight_fields = [ :country, :first_name, :last_name, :nationality ]
like_fields = [:country_preferences, :keyword, :languages ]
bool_fields = [:driver, :housework, :children ]

# cicle the options and build the query
opts.each do |k, v|
if straight_fields.include? k
fields += " AND #{k} = ?"
values += v
elsif like_fields.include? k
fields += " AND #{k} like %?%"
values += v
elsif bool_fields.include? k
fields += " AND #{k} = ?"
values += v == 'true'
else
logger.warn "opts #{k} with value #{v} ignored from search"
end
end

# execute the query and return the result
self.registered.actives.where([fields] + values)
end


I think I can use more scope but how can I combine them together? In general, how can I make this code much better?

The boss, when introducing the project, said one-to-many relationships for that kind of fields would be a pain in the back. What do you think about this pragmatic solution?

• Does the code you posted in your edit actually work? It seems like you're missing profiles = at the beginning of every line after the first and a final profiles at the end of the method. – sepp2k Feb 1 '11 at 18:40
• Good point! - I've already corrected my code and I'm not going to refactor, but that behavior (getting rid of all the assignment) can be achieved with a dynamic delegator explained by Ryan Bates railscasts.com/episodes/212-refactoring-dynamic-delegator – ecoologic Feb 11 '11 at 23:21

Have you looked into using a pre existing gem for this? I found one called has_scope that seems like it might do exactly what you're trying to refactor. You would need to add a scope for each of your filters and then add some has_scope calls in your controller.

Here's a few examples. I'm not as familiar with Rails 3 scopes as I am with 2, so the code likely won't work as is.

Model:

class Profile < ActiveRecord::Base
scope :country, lambda {|country| where(:country => country) }
scope :keyword, lambda {|keyword| where(["keyword LIKE :term", {:term => "%#{keyword}%"}]) }
scope :driver, where{:driver => true)
end


Controller:

class ProfileController < ApplicationController
has_scope :country
has_scope :keyword
has_scope :driver, :type => :boolean
end

• this plugin looks easy tiny and tested, I would avoid to install an external plugin to substitute a single function used once, but when I have some time I'm going to dive in the code, there should be something to learn, thanks – ecoologic Feb 1 '11 at 9:24
• thanks a lot, that was it, but you don't need to declare scopes like country or driver, as you can see in the question edit you have by default scoped_by_driver(true) that works the same. – ecoologic Feb 2 '11 at 9:17
• I didn't even know about the scoped_by_* methods, great tip! – Peter Brown Feb 2 '11 at 13:12