Rails Scopes: A Guide with Real-World Examples

By Saman Batool
June 26, 2025

Rails scopes are a powerful way to encapsulate database queries in a reusable and readable manner. They help in keeping code clean, DRY (Don’t Repeat Yourself), and easy to maintain. In this blog post, we’ll explore the basics of Rails scopes and then dive into a detailed breakdown of a complex scope inspired by real-world scenarios. By the end, we’ll have a solid understanding of how to leverage scopes to handle intricate query requirements.

What are Rails Scopes?

At its core, a scope in Rails is a class-level method that adds clarity and reusability to ActiveRecord queries. Instead of writing raw SQL queries or long chains of ActiveRecord methods directly in our controllers or models, we can define a scope that encapsulates the logic in a clean and reusable way.

Key Characteristics of Scopes:

  • Reusability: Scopes can be called multiple times across our applications.
  • Chaining: We can chain multiple scopes together to build complex queries.
  • Readability: Scopes make queries more readable and intention-revealing.
  • Lazy Loading: Scopes return an ActiveRecord::Relation, which means that the query is not executed until it’s needed.

Defining a Scope:

Scopes are typically defined in the model using the scope method:

class Post < ApplicationRecord scope :published, -> {where(published: true)} scope :recent, -> {order(created_at: :desc)} end

We can use these scopes like this:

Post.published # Returns all posts where the `published` field is true. Post.recent # Returns all posts ordered by their `created_at` field in descending order. Post.published.recent # Combines both scopes to return only published posts, ordered by `created_at` in descending order.

A Slightly More Advanced Example:

Let’s take a step further. Imagine we have a Task model, and we want to filter tasks based on their status. For instance, we may want to find all completed tasks created in the last week. Here’s how we can achieve this using scopes:

class Task < ApplicationRecord scope :completed, -> {where(status: 'completed')} scope :created_last_week, -> {where(created_at: 1.week.ago.beginning_of_week...1.week.ago.end_of_week)} end

We can now combine these scopes to find tasks matching both conditions:

Task.completed.created_last_week

This example showcases how scopes can be combined to create powerful, reusable queries while keeping our codebase clean.

A Complex Scope for Searching Contacts

Let’s consider a scenario where we have a Customer model, and we want to build a search feature that queries across different types of customers – such as personal, business, affiliate and institutional customers. Let’s look at how we can achieve this using a Rails scope.

Background on the Database Structure

To understand this query, let’s take a look at the relevant tables and relationships: * Customers: The main table, representing the records we are searching. * CustomerTypes: A related table defining the type of each entity (e.g. personal, business, etc). * Related Tables: Additional tables (personalcustomers, businesscustomers, affiliatecustomers, institutionalcustomers) storing specific attributes for each customer type.

Customers belong to CustomerTypes and may also be associated with one of the related tables based on their type. This structure allows us to filter and search customers dynamically through their attributes.

Building the Query Step by Step

Step 1: Start with the Base Query

We begin by joining the customer_types table, which helps us filter customers based on their type.

scope :search_for, -> (search_term) { joins(:customer_types) }

At this point, the scope only includes customers with their types joined in the query.

Step 2: Add Associations for Searching

Next, we include additional associations for the specific entity types we want to search through. Using left_outer_joins, we ensure that all customers are included in the results, even if they lack associated records in these related tables. This is useful for scenarios where some customers may not have complete data but should still be part of the query results.

scope :search_for, -> (search_term) { joins(:customer_type) .left_outer_joins(:personal_customers, :business_customers, :affiliate_customers, :institutional_customers) }

This step prepares the query for filtering based on related entities.

Step 3: Add Search Conditions

We now add a where clause to filter results based on the search term. The clause determines which customers match the search by checking for specific fields based on their type, such as personal, business, affiliate, or institutional.

For example, to handle personal customers:

scope :search_for, ->(search_term) { joins(:customer_type) .left_outer_joins(:personal_customers, :business_customers, :affiliate_customers, :institutional_customers) .where(<<~SQL.squish, search_term: “%#{search_term}%”) (customer_types.name = 'personal' AND CONCAT(personal_customers.attribute1, ' ', personal_customers.attribute2) ILIKE :search_term) SQL }

search_term - this parameter represents the input provided by the user that is matched against the customer attributes to find relevant records. squish - rails method that removes unnecessary whitespace from the SQL query, ensuring it's clean and compact. ILIKE - is a PostgreSQL operator that performs a case-insensitive search, making the query user friendly regardless of capitalization. AND CONCAT - this is a SQL function that combines (or concatenates) multiple fields into a single string for comparison. In this case, it concatenates two customer attributes (e.g. attribute1 and attribute2) into a full name to match against the search_term. This is especially useful when the search term may span across multiple fields.

Step 4: Adding More Conditions for Other Entity Types

Expand the where clause to include similar conditions for other entity types:

scope :search_for, ->(search_term) { joins(:customer_type) .left_outer_joins(:personal_customers, :business_customers, :affiliate_customers, :institutional_customers) .where(<<~SQL.squish, search_term: “%#{search_term}%”) (customer_types.name = 'personal' AND CONCAT(personal_customers.attribute1, ' ', personal_customers.attribute2) ILIKE :search_term) OR (customer_types.name = 'business' AND CONCAT(business_customers.attribute1, ' ', business_customers.attribute2) ILIKE :search_term) OR (customer_types.name = 'affiliate' AND CONCAT(affiliate_customers.attribute1, ' ', affiliate_customers.attribute2) ILIKE :search_term) OR (customer_types.name = 'institutional' AND institutional_customers.attribute1 ILIKE :search_term) SQL }

This scope now allows us to dynamically search customers across various types by matching the provided ‘search_term’ with attributes specific to each type.

Usage:

Here’s how we can use the search_for scope:

Customer.search_for(“Sample Term”)

This will return all entities matching the search term “Sample Term” across the different related models.

Why Use a Scope for This?

Defining this complex query as a scope keeps our codebase clean and reusable. Instead of repeating the same query logic in multiple places, we can simply call Entity.search_for(search_term) wherever needed.

Key Takeaways

Start with the Data: Understand the structure of our database and relationships before writing out the query.

Leverage SQL Within Scopes: Using raw SQL when ActiveRecord methods fall short can provide clarity and control over complex queries. Don’t shy away from it!

Note - Thorough testing is essential. Make sure our scope handles edge cases like missing relationships or special characters in the search term.

Rails scopes are an essential tool for simplifying database queries and improving code clarity. By using scopes, we can handle complex logic while maintaining a clean and reusable codebase. The search_for scope is a perfect example of how to handle intricate search functionality, making the application both robust and maintainable. Start small, practice building scopes and see how they transform our Rails application!