Skip
Arish's avatar

26. Scopes


Active Record Scopes

Scopes are custom queries defined in your models. They allow you to specify commonly-used queries that can be referenced as method calls and chained together.

Defining Scopes

Basic Scope Syntax

ruby
1class Article < ApplicationRecord
2  # Lambda syntax (preferred)
3  scope :published, -> { where(published: true) }
4  scope :draft, -> { where(published: false) }
5  scope :recent, -> { order(created_at: :desc) }
6  
7  # With arguments
8  scope :by_status, ->(status) { where(status: status) }
9  scope :created_after, ->(date) { where("created_at > ?", date) }
10  
11  # With default argument
12  scope :recent_count, ->(count = 10) { order(created_at: :desc).limit(count) }
13end

Using Scopes

ruby
1# Call scopes like methods
2Article.published
3Article.draft
4Article.recent
5
6# With arguments
7Article.by_status('archived')
8Article.created_after(1.week.ago)
9Article.recent_count(5)
10
11# Chain scopes together
12Article.published.recent.limit(10)

Scopes vs Class Methods

Scopes can also be defined as class methods:

ruby
1class Article < ApplicationRecord
2  # Scope syntax
3  scope :published, -> { where(published: true) }
4  
5  # Equivalent class method
6  def self.published
7    where(published: true)
8  end
9  
10  # Complex logic is better as class method
11  def self.trending
12    joins(:views)
13      .where("views.created_at > ?", 1.week.ago)
14      .group(:id)
15      .order("COUNT(views.id) DESC")
16      .limit(10)
17  end
18end

When to Use Class Methods

ruby
1class Article < ApplicationRecord
2  # Class method when you need conditional logic
3  def self.visible_to(user)
4    if user&.admin?
5      all
6    elsif user
7      where(published: true).or(where(user_id: user.id))
8    else
9      where(published: true)
10    end
11  end
12  
13  # Class method when returning non-relation
14  def self.statistics
15    {
16      total: count,
17      published: published.count,
18      draft: draft.count,
19      average_length: average(:word_count)
20    }
21  end
22end

Common Scope Patterns

Status Scopes

ruby
1class Order < ApplicationRecord
2  # Status scopes
3  scope :pending, -> { where(status: 'pending') }
4  scope :processing, -> { where(status: 'processing') }
5  scope :completed, -> { where(status: 'completed') }
6  scope :cancelled, -> { where(status: 'cancelled') }
7  
8  # Combined status scopes
9  scope :active, -> { where(status: ['pending', 'processing']) }
10  scope :inactive, -> { where(status: ['completed', 'cancelled']) }
11end

Time-Based Scopes

ruby
1class Article < ApplicationRecord
2  scope :today, -> { where("DATE(created_at) = ?", Date.today) }
3  scope :this_week, -> { where(created_at: 1.week.ago..Time.current) }
4  scope :this_month, -> { where(created_at: 1.month.ago..Time.current) }
5  scope :this_year, -> { where(created_at: 1.year.ago..Time.current) }
6  
7  scope :recent, ->(days = 7) { where("created_at > ?", days.days.ago) }
8  scope :between, ->(start_date, end_date) { where(created_at: start_date..end_date) }
9  
10  # Using beginning_of_day for accuracy
11  scope :on_date, ->(date) { 
12    where(created_at: date.beginning_of_day..date.end_of_day) 
13  }
14end

Ordering Scopes

ruby
1class Product < ApplicationRecord
2  scope :by_price, -> { order(:price) }
3  scope :by_price_desc, -> { order(price: :desc) }
4  scope :by_name, -> { order(:name) }
5  scope :newest_first, -> { order(created_at: :desc) }
6  scope :oldest_first, -> { order(created_at: :asc) }
7  scope :most_popular, -> { order(sales_count: :desc) }
8  scope :alphabetical, -> { order(:name) }
9  
10  # With nulls handling
11  scope :by_rating, -> { order(Arel.sql("rating DESC NULLS LAST")) }
12end

Search Scopes

ruby
1class User < ApplicationRecord
2  scope :search_by_name, ->(query) { 
3    where("name ILIKE ?", "%#{sanitize_sql_like(query)}%") 
4  }
5  
6  scope :search_by_email, ->(query) { 
7    where("email ILIKE ?", "%#{sanitize_sql_like(query)}%") 
8  }
9  
10  scope :search, ->(query) {
11    return all if query.blank?
12    where("name ILIKE :q OR email ILIKE :q", q: "%#{sanitize_sql_like(query)}%")
13  }
14end

Association Scopes

ruby
1class Comment < ApplicationRecord
2  belongs_to :article
3  belongs_to :user
4  
5  scope :by_article, ->(article) { where(article: article) }
6  scope :by_user, ->(user) { where(user: user) }
7  scope :with_user, -> { includes(:user) }
8  scope :with_article, -> { includes(:article) }
9end

Chaining Scopes

ruby
1class Article < ApplicationRecord
2  scope :published, -> { where(published: true) }
3  scope :recent, -> { order(created_at: :desc) }
4  scope :featured, -> { where(featured: true) }
5  scope :by_category, ->(cat) { where(category: cat) }
6  scope :with_author, -> { includes(:user) }
7end
8
9# Chain together
10Article.published.recent.featured
11Article.published.by_category('technology').limit(10)
12Article.published.with_author.recent.limit(5)
13
14# Use in controller
15def index
16  @articles = Article.published
17                     .by_category(params[:category])
18                     .recent
19                     .page(params[:page])
20end

Default Scope

Apply a scope automatically to all queries:

ruby
1class Article < ApplicationRecord
2  # All queries will include this by default
3  default_scope { order(created_at: :desc) }
4  
5  # Soft delete pattern
6  default_scope { where(deleted_at: nil) }
7  
8  scope :with_deleted, -> { unscope(where: :deleted_at) }
9end
10
11# Using default scope
12Article.all  # Already ordered by created_at desc
13Article.published  # Still ordered
14
15# Override default scope
16Article.unscoped.all
17Article.with_deleted

Warning About Default Scope

ruby
1# Default scope can cause unexpected behavior:
2class Article < ApplicationRecord
3  default_scope { where(published: true) }
4end
5
6# This might not work as expected:
7Article.create(title: "Draft", published: false)
8Article.find(1)  # May not find unpublished articles!
9
10# Better approach: use explicit scopes
11class Article < ApplicationRecord
12  scope :published, -> { where(published: true) }
13  scope :visible, -> { published.order(created_at: :desc) }
14end

Scopes with Joins

ruby
1class Article < ApplicationRecord
2  belongs_to :user
3  has_many :comments
4  has_many :likes
5  
6  scope :by_active_users, -> { joins(:user).where(users: { active: true }) }
7  scope :with_comments, -> { joins(:comments).distinct }
8  scope :most_liked, -> { 
9    left_joins(:likes)
10      .group(:id)
11      .order("COUNT(likes.id) DESC") 
12  }
13  scope :popular, -> {
14    where("comments_count > ? OR likes_count > ?", 10, 50)
15  }
16end

Merging Scopes

ruby
1class Article < ApplicationRecord
2  scope :published, -> { where(published: true) }
3end
4
5class User < ApplicationRecord
6  scope :active, -> { where(active: true) }
7  has_many :articles
8end
9
10# Merge scopes from different models
11Article.joins(:user).merge(User.active).published

Extending Scopes

ruby
1class Article < ApplicationRecord
2  scope :published, -> { where(published: true) }
3  
4  def self.published_this_week
5    published.where("created_at > ?", 1.week.ago)
6  end
7end
8
9# Or use scope extensions
10scope :published, -> { 
11  where(published: true).extending(PublishedExtensions) 
12}
13
14module PublishedExtensions
15  def this_week
16    where("created_at > ?", 1.week.ago)
17  end
18end
19
20Article.published.this_week

Scopes make your queries reusable, readable, and chainable!