Object-oriented approach to ActiveRecord

One of my colleges (the main programmer for the company I work for) has created a nice post on creating dynamic search criteria in an object-oriented way using Rails ActiveRecord. You can find his post here.

At first, I was a little out of balance, becouse I couldn’t realy see the advantage of it. But now after working on a few big rails projects, I realized quickly that this was a real neat way of generating a query in ActiveRecord.  Yet, I had to make a few changes.

First, create a class called record_finder.rb and add the following code:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class RecordFinder
 
  attr_reader :parameters
  attr_accessor :order_by 
 
  def initialize (bool_mode = 'AND')
    @bool_mode = bool_mode
    @sqls = []
    @parameters = []
    @includes = []
    @order_by = ''
  end
 
  def add (sql, *params)
    @sqls << sql
    @parameters += params
  end
 
  def add_ref(field, int)
    add "#{field.to_s} = ?", int
  end
 
  def add_wildcard(field, value)
    add "#{field.to_s} LIKE ?", "%#{value}%"
  end
 
  def add_range(field, range)
    if field.instance_of?(Hash)
      add "#{field['from']} >= ?", range['from']
      add "#{field['until']} <= ?", range['until']      
    else
      add "#{field} >= ?", range['from']
      add "#{field} <= ?", range['until']
    end      
  end
 
  def has_conditions?
    @sqls.filled?
  end
 
  def add_finder(finder)
    if finder.has_conditions?
      @sqls << finder.sql_string
      @parameters += finder.parameters
    end
  end
 
  def sql_string
    @sqls.collect{|sql| "(#{sql})"}.join(" #{@bool_mode} ")
  end
 
  def get
    if @sqls.length > 0
      [ sql_string ] + @parameters
    else
      nil
    end
  end
 
  def get_all
    options = {
      :include => @includes,
      :conditions => get,
    }
 
    if @order_by.filled?
       options[:order] = @order_by
    end
 
    return options
  end
 
  def include(path)
    unless @includes.include? path
      @includes << path
    end
  end
 
  def is_empty(var)
    return var == nil || var == ""
  end
end

The only special diffrence is the add_range action. With the add_range, you can search for a field in a sertain range. If the parameter is a Hash, you can use it to filter a record that has a start and end date. Otherwise, you just filter a range on one field.

The next step is to create a finder class that inherets from the RecordFinder class for all the resources you wish to search through. It might look like something as this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ResourceFinder < RecordFinder
  def by_user(user_id)
    add("user_id = ?", user_id)
  end
 
  def by_name(name)
    add_wildcard("name", name)
  end
 
  def read_search(search)
    if search
      self.by_name unless is_empty(search[:name])
    end
  end 
end

Now, you have 2 options on how to build your query. The first one is to call for all the actions needed in your controller like this:

1
2
3
4
5
  finder = ResourceFinder.new  
  finder.by_user(session[:user].id)
  finder.by_name(params[:search][:name])  
 
  @resource = Resource.find(:all, finder.get)

Now you build your search in your controller. But if you have a lot of search parameters, your index action could get ugly after a while. That’s why I have created the read_search action in my ResourceFinder.rb class. Just pass you search hash to it, and build your query in there. This way, you build-up is centered in the finder class and your controller stays neat and clean :)

Leave a Comment