YASD-TECH
YASD TECH
# Rails

scope で検索条件に名前をつける

投稿日:2026/7/28

更新日:2026/7/28

ttitleImage

scope で検索条件に名前をつける

scope は「検索条件に名前をつけて、モデルのメソッドとして使えるようにする」仕組み。
返すのは配列ではなく ActiveRecord::Relation なので、whereorder と同じ顔をしてチェーンの途中に挟める。

例として、注文(orders)を使う。「購入意思がある注文」を、確定日時が入っているステータスが発送準備以降に進んでいる かで判定するものとする。

ruby
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

呼び出すとクラスメソッドのように使える。

ruby
Order.confirmed        # => 確定日時が入っている注文
Order.purchase_intent  # => 確定済み or ステータスが preparing 以降の注文

-> { } は lambda(ブロック)で、呼ばれるたびに評価されるのがポイント(後述)。


価値① 意味に名前がつく

scope がないと、使う側は毎回こう書くことになる。

ruby
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

ruby
rel = Order.purchase_intent.where(canceled_at: nil).order(:created_at)
rel.class  # => Order::ActiveRecord_Relation

Relation は「まだ実行されていないクエリの設計図」なので、後ろにいくらでも条件を足せる。

ruby
Order.
  where(customer_id: customer_ids).
  purchase_intent.              # ← scope を途中に挟める
  where(canceled_at: nil).
  order(:created_at, :id).
  preload(product: :brand)

purchase_intentwhereorder と同じ位置に並べられるのが scope の便利さ。もし配列を返すただのメソッドだったら、この位置には置けない(配列に where は生えていないので、以降のチェーンが全部壊れる)。


価値③ scope 同士を組み合わせられる

purchase_intent の定義をよく見ると、中で confirmed を呼んでいる。

ruby
scope :purchase_intent, -> { confirmed.or(where(status: statuses[:preparing]..)) }
                             ~~~~~~~~~ 既存の scope を再利用

生成される SQL も、ちゃんと1つの WHERE 句にまとまる。

sql
WHERE (orders.confirmed_at IS NOT NULL OR orders.status >= 50)

条件を「部品」として持っておいて、組み合わせて意味のある単位を作れる。


引数も取れる

ruby
scope :latest, -> { where(is_latest: true) }                # 引数なし
scope :created_after, ->(time) { where(created_at: time..) } # 引数あり
ruby
Order.latest
Order.created_after(1.week.ago)

クラスメソッドとの違い

scope はほぼクラスメソッドの糖衣構文で、こう書いてもだいたい同じ動きをする。

ruby
def self.purchase_intent
  confirmed.or(where(status: statuses[:preparing]..))
end

違いは1点。scope は nil を返しても all(全件の Relation)にフォールバックするので、チェーンが壊れない。

ruby
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 がそのまま返る
向いている用途 検索条件の名前付け 分岐や複数行のロジックを含む処理

なぜ lambda で書くのか

-> { } で包むのは、呼ばれるたびに中身を再評価させるため。

ruby
# ❌ ダメな例(クラス読み込み時の 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 に詰め込むと再利用しづらくなる。意味論として独立しているものだけを scope にするのが目安。

条件 置き場所 理由
purchase_intent(購入意思があるか) モデルの scope 業務上の概念。どこから呼んでも同じ意味
canceled_at: nil(キャンセル済みを除く) 呼び出し側 「この画面/この通知では見せない」という表示上の都合

前者は「Order とは何か」を説明する条件なのでモデルの責務。後者は文脈依存で、別のバッチでは「キャンセル済みも含めたい」ことがあるので、scope に混ぜると使い回せなくなる。


関連

Index

  • scope で検索条件に名前をつける
  • 価値① 意味に名前がつく
  • 価値② チェーンできる(これが最大の利点)
  • 価値③ scope 同士を組み合わせられる
  • 引数も取れる
  • クラスメソッドとの違い
  • なぜ lambda で書くのか
  • 何を scope にして、何を呼び出し側に残すか
  • 関連