Robert Mosolgo

Move ActiveRecord Scopes Into Separate Files

Ruby on Rails models tend to grow and grow. When refactoring scopes, it turns out you can move them into their own classes.

The Problem

Rails models can get out of hand. Over time they get more associations, more methods, more everything. The resulting huge API and visual clutter makes those classes hard to maintain.

Consider these scopes:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CheckIn < ActiveRecord::Base
  scope :normal, -> { where(kind: "Regular") }
  scope :guest, -> { where(kind: "Guest") }
  scope :volunteer, -> { where(kind: "Volunteer") }
  scope :first_time, -> {
    joins(%{
      INNER JOIN person_events
        ON  person_events.person_id =         check_ins.person_id
        AND person_events.event_id =          check_ins.event_id
        AND person_events.first_check_in_id = check_ins.id
        })
  }
end

How do we usually address this?

For me, refactoring often means finding related methods & values that deserve their own class, then moving code out of the model and into the new class. For example:

  • moving complex validations into validator classes
  • moving complex serialization into serializer classes (I do this with serialization to English, too, not just JSON)
  • moving complex calculations into value classes.

Whenever I’m trying to move code out of a model, I visit Code Climate’s great post on the topic.

However, scopes are never on the list. What can we do with those?

Digging In

I poked around Rails source a bit to see if there were any other options available to me.

I found that the body passed to ActiveRecord::Base.scope just has to respond to :call. I guess that’s why lambdas are a shoo-in for that purpose: they respond to :call and aren’t picky about arguments.

The other thing I found is that the lambdas you usually pass to scope aren’t magical. I always assumed that they were instance_eval’d against other objects at whatever other times, but as far as I can tell, they aren’t magical. self is always the model class (from lexical scope), just like any other lambda.

Instead, the magic is a combination of Rails’ thread-aware ScopeRegistry which tracks the scope for a given class, combined with Association#scoping, which I don’t understand. :)

Moving Scopes from Lambda to Class

You can make a class that complies to the required API. Make calls on the model class (CheckIn, in my case), which is usually self in a scope lambda.

1
2
3
4
5
6
# app/models/check_in/scopes/latest.rb
class CheckIn::Scopes::Latest
  def call
    CheckIn.where("check_ins.id IN (SELECT max(id) FROM check_ins GROUP BY check_ins.person_id)")
  end
end

Then, hook up the scope in the model definition:

1
2
3
class CheckIn < ActiveRecord::Base
  scope :latest, Scopes::Latest.new
end

Since it’s just a plain ol’ class, you can give it other methods too:

1
2
3
4
5
6
7
8
9
10
11
12
# app/models/check_in/scopes/latest.rb
class CheckIn::Scopes::Latest
  def call
    CheckIn.where(query_string)
  end

  private

  def query_string
    "check_ins.id IN (SELECT max(id) FROM check_ins GROUP BY check_ins.person_id)"
  end
end

You can also initialize it with some data:

1
2
3
4
5
class CheckIn < ActiveRecord::Base
  scope :normal,          Scopes::KindScope.new("Regular")
  scope :guest,           Scopes::KindScope.new("Guest")
  scope :volunteer,       Scopes::KindScope.new("Volunteer")
end

Any Benefit?

Here’s what I think:

Pros:

  • Less visual noise.
  • Your model still reads like a table of contents.
  • Theoretically, you could test the scope in isolation (but I’m too lazy, if the existing tests still pass, that’s good enough for me :P).

Cons:

  • If the scope takes arguments, you can’t tell right away.
  • It doesn’t actually shrink the class’s API: it’s still a big ol’ model.
  • It’s not a known Rails practice.