zargony.com

#![desc = "Random thoughts of a software engineer"]

ActiveRecord condition building made easy

Building an ActiveRecord condition parameter can result in a lot of unattractive code if it needs to depend on various things. E.g. if you need to find records that match multiple criteria given by the user, you might end up writing a lot of code to do so.

Using a hash as a condition might work in some cases:

conditions = {}
conditions[:orderno] = orderno unless orderno.blank?
conditions[:color] = color unless color.blank?
...
Product.find(:all, :conditions => conditions)

However this only works for simple searches (where values are matched by equality). If you try to build a slightly more complex condition (like searching for a substring in the name of a product, or for products that are cheaper than a given price), a hash won't work and you need to use an array for the condition. This usually leads to clumsy code that needs to assemble the query string and its parameters in the right order depending on the given criteria. However, by extending the Array class with a helper method, everything becomes easier and nicer:

class Array
  def add_condition! (condition, conjunction = 'AND')
    if String === condition
      add_condition!([condition])
    elsif Hash === condition
      add_condition!([condition.keys.map { |attr| "#{attr}=?" }.join(' AND ')] + condition.values)
    elsif Array === condition
      self[0] = "(#{self[0]}) #{conjunction} (#{condition.shift})" unless empty?
      (self << condition).flatten!
    else
      raise "don't know how to handle this condition type"
    end
    self
  end
end

I originally found this idea in a DZone snippet and enhanced it a little bit.

Now, building the condition is nice, clean and DRY:

conditions = []
conditions.add_condition!(:orderno => orderno) unless orderno.blank?
conditions.add_condition!(:color => color) unless color.blank?
conditions.add_condition!(['name LIKE "%?%"', keyword]) unless keyword.blank?
conditions.add_condition!(['price >= ?', min_price]) unless min_price.blank?
conditions.add_condition!(['price <= ?', max_price]) unless max_price.blank?
Product.find(:all, :conditions => conditions)

Btw, if you were tempted to use a single string to build a condition, like conditions = "price >= #{max_price}", go and read about SQL injection. Never ever add user-input to your conditions without checking/escaping it first! Sadly, this seems to be a common mistake made by many people.