How to optimize your search with Named Scopes in Rails 5

When I started working with Ruby on Rails in 2010, named scopes were something I never really understood. Back then, I was not comfortable writing methods on my own or using ActiveRecord queries — and to be honest I had no idea what a database migration was. Scopes felt like one of those “automagical” parts of Rails, so I ignored them as long as I could.

Here is one of the principles of Ruby on Rails I’ve learned over the last few years: your models should be fat and your controllers should be skinny. Simply put, it means that the controllers should be straightforward — they should serve information to the views and control the adding, removing, or editing of records in your database. The model should be place where you define, filter, and refine the information you serve to the controllers.

As your application scales, simple filters in the controller should be replaced by reusable search queries and filters in the model.

This is where named scopes come in.

What is a named scope?

A named scope is simply a class method inside of your model that always returns an active record association.

Imagine this application: we have a database of shirts and we want to return all of the shirts that are red. We can do it the following ways:

  1. Both the scope and the method create a very basic filter to simply return all the shirts that are red. In the controller, we can query all of the red shirts by calling one of the methods:
#app/models/shirt.rb
scope :red, -> {where(color: "red")}
def self.red
where(color: "red")
end

This returns all red-colored shirts. But still, the filter is pretty basic, because we would need a different method for each color.

#app/controllers/shirts_controller.rb
def index
@shirts = Shirt.all.red #you can even simplify it to Shirt.red
end

2. A better execution would be to abstract the method for every possible color:

#app/models/shirt.rb
scope :colored, -> (color) {where(color: color)}
def self.colored(color)
where(colored: color)
end

Both the scope and the class method take an argument of a specific color and return all of the shirts of the color.

3. The above way of doing the where query is prone to SQL injection, so a more secure way would be to escape the query with this syntax:

#app/models/shirt.rb
scope :colored, -> (color) {where("color LIKE ?", color)
def colored(color)
where("color LIKE ?", color)
end

In the controller, you can query the search the following way:

#app/controllers/shirts_controller.rb
def index
@color = params[:color] #this could be data you get from a form
@shirts = Shirt.colored(@color)
end

In all of these examples, the named scope and the class method both returned the same data, however, the advantage of the named scope is the simplicity of writing the method in just one line.

When should you use a named scope over a class method?

One common use of scopes is the ‘soft delete’. In most cases, when you create a website with a database of users, you will not actually delete a user when they deactivate their account. This could be for devious data-keeping reasons, or simply to have the email on file in case a user wants to register with the same email again (for example, so they don’t abuse a free trial offer).

With the soft delete, when a user is deleted, they are not purged from the database, just marked as ‘inactive’. This means that when you query your database for users, you may not want to query through every single user — just the ones currently active on the site (or the ones on the paid plan, etc etc).

A scope is a really easy to way mark a user as active, like this:

#app/models/user.rb
scope :active, -> {where(status: "active")}
scope :inactive, -> {where(status: "inactive")}
#app/controllers/users_controller.rb
def index 
@users = User.all.active
end

Why does this matter?

As the application expands, you may need to filter different users with different filter — users in a specific city or with specific roles. Think of Uber, Airbnb, or another app going from serving just San Francisco and New York to dozens of cities in the US and around the world.

Because a named scope will always return an active record association — and never nil — you can safely string as many named scopes as you like and avoid the search failing when one of the filters is empty. The edge case of nil is always taken care of.

How is a scope different from a class method?

Named scopes are really great for two things: Chaining and Readability;

Chaining

A scope returns an object relation — which means that even if there are no results, it will not return nil but rather an empty array. Which means that you can string multiple scopes together without worrying about checking for nil, like this:

Article.published.featured.latest_article

Readability

Scopes can be written in just one line. As your application scales, they are a great way to keep your model dry. Think of it as writing a ternary operator instead of an if-statement with multiple lines. Their use is what separates a new programmer from a more experienced developer.

scope :recent, -> { order(:created_at, :desc) }

When should I NOT use a named scope?

The two most common reasons for not using a scope are when the search query becomes too complex or when the search negatively impacts the speed of the application.

Complexity

As mentioned above, a named scope is a perfect way to filter an application using active record queries or SQL. It’s a an easy way to write a WHERE, ORDER, LIMIT, etc with just one line. However, beyond a simple filter, a class method can be a better way to represent a more complex search within the data. A more complex method can include several scopes that are easier to read on their own.

Speed

Performance is supremely important to an application that grows over time. Every time that you call on a scope, you query the database. When you iterate over these queries, you create what is called the n+1 query — a query that eventually slows your application to a crawl. Because a scope will always query the database, instead of executing the query, you can instead pre-load it as an active record association. Justin Weiss has a fantastic article on how to pre-load scopes to avoid lag:

Write less code. Write more meaningful code. Be a happier developer.

The Takeaway

If you just skimmed this article to get to this part, there are just two major takeaways I’ll leave you with:

  1. Don’t write simple filters for your database inside the Controllers.
  2. Use named scopes to create flexible, readable, and safe search filters inside of the Model.

That’s it!

Do you often use named scopes in your Rails applications? Are there any other topics I should cover next? Let me know below!