Skip
Arish's avatar

33. Named Scopes


Named Scopes

Scopes allow you to define commonly-used queries as reusable methods. They make your code cleaner and more expressive.

Defining Scopes

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

Using Scopes

ruby
1# Use like class methods
2Article.published
3Article.draft
4Article.recent
5
6# With arguments
7Article.by_category("technology")
8Article.created_after(1.week.ago)
9Article.recent_count(5)
10
11# Chain scopes
12Article.published.recent.limit(10)
13Article.published.by_category("technology").recent

Scopes vs Class Methods

Both achieve the same result:

ruby
1class Article < ApplicationRecord
2  # As scope
3  scope :published, -> { where(published: true) }
4  
5  # As class method (equivalent)
6  def self.published
7    where(published: true)
8  end
9end

When to Use Class Methods

ruby
1class Article < ApplicationRecord
2  # Use class methods for complex 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  # Or when returning non-relation
14  def self.statistics
15    {
16      total: count,
17      published: published.count,
18      draft: draft.count
19    }
20  end
21end

Common Scope Patterns

Status Scopes

ruby
1class Order < ApplicationRecord
2  # Status-based 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: %w[pending processing]) }
10  scope :finished, -> { where(status: %w[completed cancelled]) }
11  
12  # Not status
13  scope :not_cancelled, -> { where.not(status: "cancelled") }
14end

Time-Based Scopes

ruby
1class Article < ApplicationRecord
2  scope :today, -> { where(created_at: Date.today.all_day) }
3  scope :yesterday, -> { where(created_at: Date.yesterday.all_day) }
4  scope :this_week, -> { where(created_at: 1.week.ago..) }
5  scope :this_month, -> { where(created_at: 1.month.ago..) }
6  scope :this_year, -> { where(created_at: 1.year.ago..) }
7  
8  scope :between, ->(start_date, end_date) { 
9    where(created_at: start_date..end_date) 
10  }
11  
12  scope :on_date, ->(date) { 
13    where(created_at: date.beginning_of_day..date.end_of_day) 
14  }
15  
16  scope :older_than, ->(days) { where("created_at < ?", days.days.ago) }
17  scope :newer_than, ->(days) { where("created_at > ?", days.days.ago) }
18end

Ordering Scopes

ruby
1class Product < ApplicationRecord
2  scope :by_price, ->(direction = :asc) { order(price: direction) }
3  scope :by_name, -> { order(:name) }
4  scope :cheapest_first, -> { order(price: :asc) }
5  scope :expensive_first, -> { order(price: :desc) }
6  scope :newest_first, -> { order(created_at: :desc) }
7  scope :oldest_first, -> { order(created_at: :asc) }
8  scope :most_popular, -> { order(sales_count: :desc) }
9  scope :alphabetical, -> { order(:name) }
10  scope :random, -> { order(Arel.sql("RANDOM()")) }
11end

Search Scopes

ruby
1class User < ApplicationRecord
2  scope :search_name, ->(query) { 
3    where("name ILIKE ?", "%#{sanitize_sql_like(query)}%") 
4  }
5  
6  scope :search_email, ->(query) { 
7    where("email ILIKE ?", "%#{sanitize_sql_like(query)}%") 
8  }
9  
10  scope :search, ->(query) {
11    return all if query.blank?
12    
13    search_name(query).or(search_email(query))
14  }
15  
16  scope :filter_by_role, ->(role) {
17    return all if role.blank?
18    where(role: role)
19  }
20end

Pagination Scope

ruby
1class Article < ApplicationRecord
2  scope :page, ->(page_number, per_page = 25) {
3    limit(per_page).offset((page_number.to_i - 1) * per_page)
4  }
5end
6
7# Usage
8Article.published.recent.page(2, 10)

Chaining Scopes

ruby
1class Article < ApplicationRecord
2  scope :published, -> { where(published: true) }
3  scope :featured, -> { where(featured: true) }
4  scope :recent, -> { order(created_at: :desc) }
5  scope :by_author, ->(author) { where(author_id: author.id) }
6  scope :in_category, ->(category) { where(category: category) }
7end
8
9# Chain multiple scopes
10Article.published
11       .featured
12       .recent
13       .in_category("tech")
14       .limit(10)
15
16# In controller
17def index
18  @articles = Article.published
19                     .in_category(params[:category])
20                     .by_author(params[:author_id])
21                     .recent
22                     .page(params[:page])
23end

Default Scope

ruby
1class Article < ApplicationRecord
2  # All queries include this by default
3  default_scope { order(created_at: :desc) }
4  
5  # Or for soft delete
6  default_scope { where(deleted_at: nil) }
7  
8  # Override with unscoped
9  scope :with_deleted, -> { unscope(where: :deleted_at) }
10end
11
12# Usage
13Article.all  # Already ordered
14Article.unscoped.all  # Without default scope
15Article.with_deleted  # Include deleted records

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 creates a PUBLISHED article, not draft!
7Article.create(title: "Draft", published: false)
8# The published: true from default scope may override!
9
10# Better: use explicit scopes
11class Article < ApplicationRecord
12  scope :visible, -> { where(published: true) }
13end

Merging Scopes

ruby
1class Article < ApplicationRecord
2  scope :recent, -> { where("created_at > ?", 1.week.ago) }
3end
4
5class User < ApplicationRecord
6  scope :active, -> { where(active: true) }
7end
8
9# Merge scopes from different models
10Article.joins(:user).merge(User.active)

Lambda Scopes with Associations

ruby
1class User < ApplicationRecord
2  has_many :articles
3  has_many :published_articles, -> { where(published: true) }, class_name: "Article"
4  has_many :recent_articles, -> { order(created_at: :desc).limit(5) }, class_name: "Article"
5end
6
7user.published_articles
8user.recent_articles

Scopes make your models expressive and your controllers clean!