Monday 24 September 2012

Learn and Love the Scope Method

Learn and Love the Scope Method

If you want to optimize your code and minimize the complexity we can increasing the opportunity for code reuse is by leveraging the Active Record scoping methods.

For Example

class RemoteProcess < ActiveRecord::Base
  def self.find_top_running_processes(limit=5)
    find(:all, 
            :conditions => "state = 'running'",
            :order => "percent_cpu desc",
            :limit => limit)
  end


 def self.find_top_running_system_processes(limit=5)
  find(:all,
          :conditions => "state = 'running' and ( owner in ('root', 'mysql') )",
          :order => "percent_cpu desc",
          :limit => limit)
 end
end



We can clean up this method and make the components reusable by employing named scopes. The scope method defines class methods on your model that can be chained together and combined into one SQL query.

A scope can be defined by a hash of options that should be merge into the find call or by a lambda that can take arguments and return such a hash.

When you call a scope, you get back an ActiveRecord::Relation object, which walks and talks just like the array you would have gotten back from find.

You could use scopes as follows the rewrite the preceding finder:


class RemoteProcess < ActiveRecord::Base
  scope :running, where(:state => 'Running')
  scope :system,    where(:owner => ['root','mysql'])
  scope :sorted,     order("percent_cpu desc")
  scope :top,          lambda {|1| limit(1) }
end

RemoteProcess.running.sorted.top(10)
RemoteProcess.running.system.sorted.top(5)


We can shore this up nicely by wrapping the chain in a descriptive class method:

class RemoteProcess < ActiveRecord::Base
  scope :running, where(:state => 'Running')
  scope :system,    where(:owner => ['root', 'mysql'])
  scope :sorted,     order("percent_cpu desc")
  scope :top,          lambda { |1| limit(1) }

  def self.find_top_running_processes(limit=5)
   running.sorted.top(limit)
  end

  def self.find_top_running_system_processes(limit=5)
    running.system.sorted.top(limit)
  end
end

Example 2

Now we've taken a quick look at basic scope usage, let's return to the original problem of writing advanced search methods.

For Example

class Song < ActiveRecord::Base
  def self.search(title, artist, genre, published, order, limit, page)
    condition_values = { :title => "%#{title}%", 
                                       :artist => "%#{artist}%", 
                                       :genre => "%#{genre}%"
                                     }
    case order
    when "name"  :   order_clause = "name DESC"
    when "length" :   order_clause = "duration ASC"
    when "genre"  :   order_clause = "genre DESC"
    else
       order_clause = "album DESC"
    end
    joins = []
    conditions = []
    conditions << "(title LIKE ':title')" unless title.blank?
    conditions << "(artist LIKE ':artist')" unless artist.blank?
    conditions << "(genre LIKE ':genre')" unless genre.blank?

    unless published.blank?
      conditions << "(published_on == :true OR published_on IS NOT NULL)"
    end

    find_opts = { :conditions => [ conditions.join("AND"), condition_values ],
                           :joins => joins.join(''),
                           :limit => limit,
                           :order => order_clause }

   page = 1 if page.blank?
   paginate(:all, find_opts.merge(:page => page, :per_page => 25))
  end
end

we can clean up the preceding method by employing scopes as follows:

class Song <  ActiveRecord::Base
  def self.top(number)
    limit(number)
  end

  def self.matching(column,value)
     where(["#{column} like ?, "%#{value}"])
  end

  def self.published
    where("published on is not null")
  end

  def self.order(col)
    sql = case col
                when "name" : "name desc"
                when "length"  : "duration asc"
                when "genre" : "genre desc"
                else "album desc"
             end
       order(sql)
  end

  def self.search(title, artist,genre, published)
   finder = matching(:title, title)
   finder = finder.matching(:artist, artist)
   finder = finder.matching(:genre, genre)
   finder = finder.published unless published.blank?
   return finder
  end

  Song.search("fool", "billy", "rock", true).order("length).top(10).paginate(:page => 1)
end

While this re-implementation using scopes reduces the code size somewhat, the real benefits are elsewhere

Scope object instead of a results array:

class Song < ActiveRecord::Base
  has_many :uploads
  has_many :users, :through => :uploads

  # top and order are implemented the same as before,
  # using named_scope...

  def self.search(title, artist, genre, published)
    finder =            where(["title LIKE ? ", "%#{title}%"])
   finder =  finder.where(["artist LIKE ? ", "%#{artist}%"])
   finder =  finder.where(["genre LIKE ? ", "%#{genre}%"])
   unless published.blank?
     finder = finder.where("published_on is not null")
   end
  return finder
  end
end


No comments:

Post a Comment