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) }
13endUsing 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
18endWhen 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
22endCommon 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']) }
11endTime-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 }
14endOrdering 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")) }
12endSearch 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 }
14endAssociation 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) }
9endChaining 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])
20endDefault 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_deletedWarning 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) }
14endScopes 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 }
16endMerging 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).publishedExtending 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_weekScopes make your queries reusable, readable, and chainable!
