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.rbclass UsersController < ApplicationController def index @users = User.where(active: true) endend # app/controllers/users/recent_signups_controller.rbclass Users::RecentSignupsController < ApplicationController def index @users = User.where(active: true).where('created_at < ?', 1.week.ago) endend
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.rbclass 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.rbclass UsersController < ApplicationController def index @users = User.active endend # app/controllers/users/recent_signups_controller.rbclass Users::RecentSignupsController < ApplicationController def index @users = User.active.where('created_at < ?', 1.week.ago) endend
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.rbclass User < ApplicationRecord scope :active, -> { where.not(active: nil) } scope :recent, -> { where('created_at < ?', 1.week.ago }end
# app/controllers/users_controller.rbclass UsersController < ApplicationController def index @users = User.active endend # app/controllers/users/recent_signups_controller.rbclass Users::RecentSignupsController < ApplicationController def index @users = User.active.recent endend
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.