Jason Charnes

The Importance of Scopes in Ruby on Rails

Using scopes in Ruby on Rails can help you write better, testable, more modular code. Let’s walk through a couple of use cases for scopes.

First of all, if you’ve ever used Ruby on Rails, you’ve probably used ActiveRecord. (Hopefully not in your views!)

User.all # => "SELECT * FROM users;"

ActiveRecord is a robust toolset. Specifically, let’s look at querying data. You’re working with a users table with a flag representing if the user is active.

User.where(active: true) # => "SELECT * FROM users WHERE active IS TRUE;"

The check for active users is a chunk of code you’d likely reuse throughout your application.

# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.where(active: true)
end
end
 
# app/controllers/users/recent_signups_controller.rb
class Users::RecentSignupsController < ApplicationController
def index
@users = User.where(active: true).where('created_at < ?', 1.week.ago)
end
end

One day, requirements change.

SURPRISE

A user record should be inactive until a user confirms their account. It’s been decided to change the active flag from a Boolean to a DateTime record. Your new query for an active user becomes the following.

User.where.not(active: nil)

If a user account is inactive, the active flag is nil. Easy change, right? What happens, though, when your app has multiple queries for admin as a boolean in places you’re not even aware.

Ruby on Rails has a way for this problem to be mitigated. Scopes.

1. Scopes DRY Up Your ActiveRecord Calls

Using ActiveRecord, your query to active can live in the model.

# app/models/user.rb
class User < ApplicationRecord
scope :active, -> { where.not(active: nil) }
end

The scope is callable on the User, just like before. Except now it has a name.

# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.active
end
end
 
# app/controllers/users/recent_signups_controller.rb
class Users::RecentSignupsController < ApplicationController
def index
@users = User.active.where('created_at < ?', 1.week.ago)
end
end

If your underlying structure changes, similar to active changing from a Boolean to a DateTime, the call can be updated in a centralized location.

2. Scopes Are Chainable

In the Users::RecentSignupsController there is another call after .active that checks when the record was created. It’s possible this too may be reused. We can put it into a scope as well.

# app/models/user.rb
class User < ApplicationRecord
scope :active, -> { where.not(active: nil) }
scope :recent, -> { where('created_at < ?', 1.week.ago }
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.active
end
end
 
# app/controllers/users/recent_signups_controller.rb
class Users::RecentSignupsController < ApplicationController
def index
@users = User.active.recent
end
end

ActiveRecord provides the ability to chain these scopes together. You’re not limited to the number of scopes you could chain.

3. Scopes Make Testing Easier

If you’re looking to avoid the database (which isn’t always the right answer to me, but that’s another conversation,) scopes allow for easier stubbing.

describe "a useless test" do
let(:some_users) { ["Bob", "Mary", "Jonathan Long"] }
let(:users) { allow(User).to receive(:active) { some_users } }
end

Now, any call in the code your testing that uses the active scope will return pre-defined data instead of having to call the database. Stubbing the call can also save the need to pre-create a bunch of users.

Conclusion

Scopes are a powerful, important feature of ActiveRecord. They can be used to centralize queries, making your application easier to maintain and DRY.

Follow me on Twitter.