ruby
32 lines · 8 steps
How scopes compose in Rails
An Article model layers named scopes that chain into complex, reusable queries.
Explained by
highlit
1class Article < ApplicationRecord
2 belongs_to :author, class_name: "User"
3 has_many :taggings, dependent: :destroy
4 has_many :tags, through: :taggings
5
6 scope :published, -> { where.not(published_at: nil) }
7 scope :draft, -> { where(published_at: nil) }
8
9 scope :published_before, ->(time) { published.where(published_at: ..time) }
10
11 scope :in_category, ->(slug) { joins(:category).where(categories: { slug: }) }
12
13 scope :tagged_with, ->(*names) do
14 next all if names.empty?
15
16 joins(:tags)
17 .where(tags: { name: names.flatten })
18 .group(:id)
19 .having("COUNT(DISTINCT tags.id) = ?", names.flatten.uniq.size)
20 end
21
22 scope :search, ->(query) do
23 return all if query.blank?
24
25 term = "%#{sanitize_sql_like(query)}%"
26 where("title ILIKE :term OR body ILIKE :term", term:)
27 end
28
29 scope :recent, ->(limit = 10) { published.order(published_at: :desc).limit(limit) }
30
31 scope :by_author, ->(author) { where(author:) }
32end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Scopes return relations, so they chain and compose into larger queries without duplicating SQL.
- 2Guarding a scope with `all` keeps it chainable even when its argument is empty or blank.
- 3Bind parameters and `sanitize_sql_like` keep user input from becoming SQL injection.
Related explainers
ruby
class LRUCache def initialize(capacity) raise ArgumentError, "capacity must be positive" unless capacity.positive?
How an LRU cache works in Ruby
caching
eviction
hash-ordering
Intermediate
7 steps
ruby
class QueryParser def self.parse(query_string) new(query_string).parse end
Parsing nested query strings in Ruby
parsing
recursion
tokenization
Intermediate
9 steps
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
ruby
class CreateOrderItems < ActiveRecord::Migration[7.1] def change create_table :order_items do |t| t.references :order, null: false, foreign_key: { on_delete: :cascade }
Enforcing order-item integrity in Rails
migrations
foreign-keys
validations
Intermediate
7 steps
ruby
class Registration < ApplicationRecord belongs_to :event validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
Validating registrations in Rails
validations
i18n
error-handling
Intermediate
8 steps
ruby
class ApacheLogParser LINE_PATTERN = /\A(?<ip>\S+)\s\S+\s\S+\s\[(?<time>[^\]]+)\]\s"(?<method>[A-Z]+)\s(?<path>\S+)\s(?<protocol>[^"]+)"\s(?<status>\d{3})\s(?<bytes>\d+|-)/ TIME_FORMAT = "%d/%b/%Y:%H:%M:%S %z"
Parsing Apache logs with named captures
regex
named-captures
parsing
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-scopes-compose-in-rails-explained-ruby-c33e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.