DRY Scopes

One of the things you hear over and over again in the Ruby community is DRY,DRY,DRY (don’t-repeat-yourself, for the uninitiated). A couple months ago I noticed that I was making the same changes over and over again in a weird part of the codebase. It happened to be in our scopes. We had a smattering of different scopes dealing with dates and we had several different variations on the simple :between scope. We also had different selections of the dates scopes in different models. Some had the :after scope and some didn’t, etc.

So what’s a good programmer to do? DRY it up!

I first moved all of the scopes into a module and just included that, and lo-and-behold the specs still passed. Problem was though that I couldn’t do this for all of the models since some relied on a column other than created_at. So instead of just creating a separate module, I just made the current one a little more dynamic.

Here are the first two methods that start the magic:

def include_date_scopes
  include_date_scopes_for :created_at

def include_date_scopes_for(column, prepend_name = false)
  return unless self.table_exists?
  if self.columns_hash[column.to_s].type == :datetime
    define_timestamp_scopes_for column, prepend_name
  elsif self.columns_hash[column.to_s].type == :date
    define_date_scopes_for column, prepend_name

The first method is just there to make it easy to include the scopes for a default created_at column. The second one looks at the table to make sure it exists (it gets tripped up initially if the database isn’t loaded). Then it looks to see if the column is either date or datetime/timestamp type to define the scopes appropriately.

def define_timestamp_scopes_for(column_name, prepend_name = false)
  prefix = prepend_name ? "#{column_name}_" : ""

  define_singleton_method :"#{prefix}before" do |time| 
    where{__send__(column_name).lt (time.is_a?(Date) ? time.to_time : time)}

The next method actually defines the scopes. In this case they are more method than scope, but Rails will still treat them the same. This is just one of them, but you can use this as a template. And for the curious, that is not normal scope style. I am using the Squeel gem to make it much easier to read.

Now all I need to do is delete the scopes from a model and add this:


In my code I have it defining 16 different timestamp scopes and 12 different date scopes. You can really get creative with it too. If you have other common queries you run in different models, try and group them together.

Comments are closed.