Ruby on Rails: Scopes


Scopes are super useful - and easy to write. Rails 4 and newer make it really easy. Rails 3 and older do have scopes, but I won't cover them here. Let's say I have a timelog model which, among other things, has a start_time and end_time. If I wanted to scope the hard way, using the usual syntax, it would look something like:

# app/models/timelog.rb

scope = Timelog.all
scope = scope.where('start_time > ?', start_time) unless start_time.blank?
scope = scope.where('end_time < ?', end_time) unless end_time.blank?

It would be pretty lame to have to do that every time you're looking for timelogs in a particular date range. Scopes makes this super easy. Let's tweak the previous code block a bit. In our model file, timelog.rb, we can place this new block of code somewhere near the top. I like to do it below my validations and callbacks, but it's up to you.

# app/models/timelog.rb

scope :between, -> (start_time, end_time) {
  scope = all
  scope = scope.where('start_time > ? ', start_time) unless start_time.blank?
  scope = scope.where('end_time < ?', end_time) unless end_time.blank?
  return scope
}

Pretty similar save a few things here and there - but now look how easy it is to call this scope using a rails console:

2.3.1 :001 > Timelog.between(2.months.ago, Time.now)
  Timelog Load (10.9ms)  SELECT "timelogs".* FROM "timelogs" WHERE (start_time > '2017-04-18 06:47:25.635676' ) AND (end_time < '2017-06-18 06:47:25.635821')  ORDER BY "timelogs"."start_time" DESC
 => #<ActiveRecord::Relation [...]>
2.3.1 :002 > Timelog.between(2.months.ago, Time.now).count
   (0.8ms)  SELECT COUNT(*) FROM "timelogs" WHERE (start_time > '2017-04-18 06:48:30.847610' ) AND (end_time < '2017-06-18 06:48:30.847920')
 => 579

What's even better, is that the scope will accept nil for either of the bounds. So, if you wanted to get all the timelogs older than 2 weeks, it's as simple as:

2.3.1 :001 > Timelog.between(nil, 2.weeks.ago)

It's worth using scopes, clearly, but also avoid overusing them. Happy hacking!