投稿日:2026/7/28
更新日:2026/7/28

scope は「検索条件に名前をつけて、モデルのメソッドとして使えるようにする」仕組み。
返すのは配列ではなく ActiveRecord::Relation なので、where や order と同じ顔をしてチェーンの途中に挟める。
例として、注文(orders)を使う。「購入意思がある注文」を、確定日時が入っている か ステータスが発送準備以降に進んでいる かで判定するものとする。
class Order < ApplicationRecord
enum :status, { draft: 0, cart: 10, preparing: 50, shipped: 60, delivered: 70 }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :purchase_intent, -> { confirmed.or(where(status: statuses[:preparing]..)) }
end
呼び出すとクラスメソッドのように使える。
Order.confirmed # => 確定日時が入っている注文
Order.purchase_intent # => 確定済み or ステータスが preparing 以降の注文
-> { } は lambda(ブロック)で、呼ばれるたびに評価されるのがポイント(後述)。
scope がないと、使う側は毎回こう書くことになる。
customer.orders.
where.not(confirmed_at: nil).
or(customer.orders.where(status: 50..))
これが customer.orders.purchase_intent の一言になる。
「なぜ 50 なのか」「なぜ OR なのか」という業務ルールがモデルに1箇所だけ書かれるので、条件が変わったときの修正も1箇所で済む。ジョブやサービスクラス側に同じ判定メソッドを持たせると、条件が2箇所に散ってズレていく。
scope が返すのは配列ではなく ActiveRecord::Relation。
rel = Order.purchase_intent.where(canceled_at: nil).order(:created_at)
rel.class # => Order::ActiveRecord_Relation
Relation は「まだ実行されていないクエリの設計図」なので、後ろにいくらでも条件を足せる。
Order.
where(customer_id: customer_ids).
purchase_intent. # ← scope を途中に挟める
where(canceled_at: nil).
order(:created_at, :id).
preload(product: :brand)
purchase_intent が where や order と同じ位置に並べられるのが scope の便利さ。もし配列を返すただのメソッドだったら、この位置には置けない(配列に where は生えていないので、以降のチェーンが全部壊れる)。
purchase_intent の定義をよく見ると、中で confirmed を呼んでいる。
scope :purchase_intent, -> { confirmed.or(where(status: statuses[:preparing]..)) }
~~~~~~~~~ 既存の scope を再利用
生成される SQL も、ちゃんと1つの WHERE 句にまとまる。
WHERE (orders.confirmed_at IS NOT NULL OR orders.status >= 50)
条件を「部品」として持っておいて、組み合わせて意味のある単位を作れる。
scope :latest, -> { where(is_latest: true) } # 引数なし
scope :created_after, ->(time) { where(created_at: time..) } # 引数あり
Order.latest
Order.created_after(1.week.ago)
scope はほぼクラスメソッドの糖衣構文で、こう書いてもだいたい同じ動きをする。
def self.purchase_intent
confirmed.or(where(status: statuses[:preparing]..))
end
違いは1点。scope は nil を返しても all(全件の Relation)にフォールバックするので、チェーンが壊れない。
scope :maybe, ->(flag) { where(x: 1) if flag }
Order.maybe(false).count # => 全件(チェーンは続く)
def self.maybe(flag) = (where(x: 1) if flag)
Order.maybe(false).count # => NoMethodError(nil に count)
条件付きで絞りたいときは scope のほうが安全。
| scope | クラスメソッド | |
|---|---|---|
| チェーン | できる | できる(Relation を返せば) |
| nil を返したとき | all にフォールバック |
nil がそのまま返る |
| 向いている用途 | 検索条件の名前付け | 分岐や複数行のロジックを含む処理 |
-> { } で包むのは、呼ばれるたびに中身を再評価させるため。
# ❌ ダメな例(クラス読み込み時の 1.day.ago で固定される)
scope :recent, where('created_at > ?', 1.day.ago)
# ⭕ 正しい(呼ぶたびに 1.day.ago が計算される)
scope :recent, -> { where('created_at > ?', 1.day.ago) }
サーバーを起動しっぱなしにすると前者は日付がズレていく、という古典的なバグ。今の Rails では lambda 必須になっている。
全部を scope に詰め込むと再利用しづらくなる。意味論として独立しているものだけを scope にするのが目安。
| 条件 | 置き場所 | 理由 |
|---|---|---|
purchase_intent(購入意思があるか) |
モデルの scope | 業務上の概念。どこから呼んでも同じ意味 |
canceled_at: nil(キャンセル済みを除く) |
呼び出し側 | 「この画面/この通知では見せない」という表示上の都合 |
前者は「Order とは何か」を説明する条件なのでモデルの責務。後者は文脈依存で、別のバッチでは「キャンセル済みも含めたい」ことがあるので、scope に混ぜると使い回せなくなる。