Active Recordのallが持つ条件を引き継ぐ性質と動的クエリ構築への応用

24 views Post
wakairo @wakairo

RailsのActive Recordのallメソッドは、 その名前から「テーブルに存在する全レコードを取得するメソッド」と理解されがちです。 しかし実際には「その時点までに積み上げられた条件に該当するものを全て取得する」という実務上とても重要な性質を持っています。

本記事では、このallの性質の基本と、動的にデータベースクエリを組み立てる際の実践的な応用テクニックについて解説します。

allは「構築済みの条件」を引き継ぐ

データベースにPenモデルのレコードが3件保存されているとします。 以下のようにPenクラスに対して直接allを呼び出すと、テーブルに存在する全てのレコードが取得されます。

test(dev):000> Pen.all
  Pen Load (1.2ms)  SELECT "pens".* FROM "pens" /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x00007cdf641ba6f0
  id: 1,
  color: "red",
  price: 80,
  created_at: "2026-02-20 05:52:53.460900000 +0000",
  updated_at: "2026-02-20 05:52:53.460900000 +0000">,
 #<Pen:0x00007cdf66b23208
  id: 2,
  color: "blue",
  price: 80,
  created_at: "2026-02-20 05:53:03.457025000 +0000",
  updated_at: "2026-02-20 05:53:03.457025000 +0000">,
 #<Pen:0x00007cdf66b230c8
  id: 3,
  color: "red",
  price: 150,
  created_at: "2026-02-20 05:53:10.752331000 +0000",
  updated_at: "2026-02-20 05:53:10.752331000 +0000">]

では、別の条件に続けてallを呼び出すとどうなるでしょうか。 以下の実行例を見ると分かるように、allは「全て」ではなく、「それまでの条件を満たすもの全て」を返します。 ここではcolor: "red"を満たすレコードだけが取得されています。

test(dev):000> Pen.where(color: "red").all
  Pen Load (0.1ms)  SELECT "pens".* FROM "pens" WHERE "pens"."color" = 'red' /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x00007cdf66b20648
  id: 1,
  color: "red",
  price: 80,
  created_at: "2026-02-20 05:52:53.460900000 +0000",
  updated_at: "2026-02-20 05:52:53.460900000 +0000">,
 #<Pen:0x00007cdf66b20508
  id: 3,
  color: "red",
  price: 150,
  created_at: "2026-02-20 05:53:10.752331000 +0000",
  updated_at: "2026-02-20 05:53:10.752331000 +0000">]

このように、allは単に全件取得する機能だけでなく、 手前に条件がある場合には「そこまでに構築されたクエリ条件(Relationオブジェクト)を引き継ぎ、そのまま返す」という機能を担う側面があります。

応用例:動的クエリ構築においてallを起点にしてメソッドチェーン可能にする

この「そこまでに構築された条件を引き継ぐ」というallの性質は、 「URLのクエリパラメータが存在する場合だけ条件を追加する」といった動的なデータベースクエリ構築において役立ちます。

例えば、クエリパラメータを処理するfilter_byというスコープを定義することを考えてみましょう。 このスコープは、以下のように「1年以内に作成されたレコード」という前提条件(where)のあとにメソッドチェーンで呼び出される想定です。

test(dev):000> Pen.where(created_at: 1.year.ago..).filter_by(color: "red", max_price: 100)
  Pen Load (0.4ms)  SELECT "pens".* FROM "pens" WHERE "pens"."created_at" >= '2025-02-21 05:50:22.982591' AND "pens"."color" = 'red' AND "pens"."price" <= 100 /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x000072d2c03393d8
  id: 1,
  color: "red",
  price: 80,
  created_at: "2026-02-20 05:52:53.460900000 +0000",
  updated_at: "2026-02-20 05:52:53.460900000 +0000">]

この挙動を実現するfilter_byスコープの実装は以下のようになります。 最初にallを呼び出して現在のRelation(既に構築済みのwhere条件など)を変数pensに格納し、 そのpensに対してパラメータの有無をチェックしながら条件を継ぎ足しています。

class Pen < ApplicationRecord
  scope :filter_by, ->(params) do
    pens = all
    pens = pens.where(color: params[:color]) if params[:color].present?
    pens = pens.where(price: ..params[:max_price]) if params[:max_price].present?
    pens
  end
end

ここで重要になるのが、filter_byの中で最初にallを呼んで変数に入れている点です。
allは「何も条件がない状態」ではなく、 「このスコープが呼ばれた時点のRelationをそのまま受け取る」役割を果たしています。 そのため、Pen.where(created_at: …)のように手前で条件を付けていても、 それをリセットすることなくさらに条件を積み上げられます。 つまり、allを変数に入れることで、 手前の条件を維持した上でさらに条件を積み上げていくための「起点」を作ることができるのです。 なお、クエリパラメータが全て空で条件追加がない場合でも、allが返した元のRelationがpensに入っているため、 メソッドチェーンをそのまま継続できます。

(参考)個別スコープへの切り出しとそのタイミング

先ほどの例ではfilter_byの内部で変数に再代入しながら条件を追加していましたが、 もしcolorpriceによる絞り込みをアプリケーション内の他の場所で単独で利用したい場合は、 それぞれを個別のスコープとして切り出すことでコードをよりキレイに書くことができます。

Active Recordのスコープは、ブロックの評価結果がnilまたはfalseになった場合、 自動的に元のRelationをそのまま返す(条件を追加せずにスルーする)という便利な仕様になっています。 そのため、以下のようにメソッドチェーンでそのままつなぐだけで、条件分岐をスコープの中に自然にカプセル化できます。

class Pen < ApplicationRecord
  scope :with_color, ->(color) { where(color: color) if color.present? }
  scope :with_max_price, ->(max_price) { where(price: ..max_price) if max_price.present? }
  scope :filter_by, ->(params) do
    with_color(params[:color]).with_max_price(params[:max_price])
  end
end

なお、YAGNI原則の観点からは、 これらの個別スコープ(with_colorwith_max_price)が他の場所で「本当に必要になるまで」は、 無理に切り出さない方が良いでしょう。 つまり、最初から再利用性を過剰に意識してスコープを量産するよりは、 まずは前述のpens = allを使うアプローチで1箇所にまとめておき、 個別スコープが本当に必要になったタイミングで切り出す方が、 結果的には開発コストを減らせるはずです。

0
Raw
https://www.techtips.page/en/comments/1114