Railsモデルのクラスメソッドはメソッドチェーンでも呼べる ― 注意点とscopeへ整理する方針

26 views Post
wakairo @wakairo

Railsでモデルにクラスメソッドを定義すると、メソッドの内容に関わらず、メソッドチェーン(ActiveRecord::Relation)経由でもそのメソッドを呼び出せます。 これが便利な場合がある一方で、エラーにはならないものの、意味的に不正確なコードが書けてしまったり、気を付けないとバグの温床になったりすることもあります。 特に、リレーションを返す用途のクラスメソッドと、そうでないクラスメソッドが混在している場合に混乱が生じやすくなります。 本記事ではその仕組みと注意点、そして実務上の整理方針を解説します。

モデルのクラスメソッドがActiveRecord::Relation経由でも呼べる仕組み

以下のようにPenクラスにクラスメソッドを定義したとします。

class Pen < ApplicationRecord
  def self.with_color(color)
    if color.present?
      where(color: color)
    else
      all
    end
  end
end

定義したこのメソッドは以下のように呼べます。これはRubyにおける通常のクラスメソッド呼び出しです。

test(dev):000> Pen.with_color("red")
  Pen Load (0.1ms)  SELECT "pens".* FROM "pens" WHERE "pens"."color" = 'red' /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x00007b3619af2060
  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:0x00007b361a437f80
  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">]

では、メソッドチェーンでの呼び出しはどうなっているでしょうか。

まず、whereなどのスコープメソッドを呼び出した結果は、以下で確認できる通り、 PenクラスではなくPen::ActiveRecord_Relationのインスタンスになります。

test(dev):000> Pen.where(created_at: 1.year.ago..).class
=> Pen::ActiveRecord_Relation

このリレーションインスタンスに対しても、クラスメソッドを以下のように呼び出せます。 つまり、モデルに定義したクラスメソッドは、そのモデルに対応したActiveRecord::Relationクラスのインスタンスからも呼べる仕組みになっているのです。この仕組みが、クラスメソッドをメソッドチェーンから呼べるようにしています。

test(dev):000> Pen.where(created_at: 1.year.ago..).with_color("red").count
  Pen Count (0.5ms)  SELECT COUNT(*) FROM "pens" WHERE "pens"."created_at" >= '2025-02-21 07:00:00.454690' AND "pens"."color" = 'red' 
=> 2

メソッドチェーンで呼べてしまうからこそ、モデルのクラスメソッドは責務と名前を明確にする

with_colorのようにクエリを組み立てるメソッドであれば、メソッドチェーンで呼び出せても違和感がないと思います。 しかし、データベースクエリと関係のないメソッドでもクラスメソッドであれば同様にリレーション経由で呼び出せてしまいます。

その結果、クラスメソッドであれば、その処理内容に関わらず、クエリを組み立てるメソッドを間に挟んだメソッドチェーンにおいて、エラーにならず実行できてしまいます。 具体例を見てみましょう。export_to_csvがクラスメソッドだとすると以下のようにwhereを間に挟んでもエラーにならずに実行できます。

Pen.where(created_at: 1.year.ago..).export_to_csv(1, 2)

ではexport_to_csvメソッドは、countのように「条件に合ったレコードに対する処理をするメソッド」なのでしょうか? それとも、クエリ構築とは関係がない「ユーティリティメソッド」で、このメソッドチェーンで呼び出すコードは何かしらのミスの結果なのでしょうか?

この例から分かるのは、モデルクラスに定義するクラスメソッドの名前はより慎重に付けなくてはならないということです。 名前だけではありません。メソッドの責務についても、クエリ用途であるか否かなど、明確さがあることが望まれます。

そのため、モデルのクラスメソッドの責務と名前に一定の規則性を持たせることも1つのアイデアです。 例えば、「findcreateなど、Active Recordのクエリメソッドで使われる単語から名前が始まる場合は、必ずクエリ関連の責務とする」といったルールが考えられます。

また、名前だけでなくコードドキュメントにおいても、他のメソッド以上に使い方をはっきりと示すことが望まれます。

クエリ構築用クラスメソッドでelse節を省くとメソッドチェーンの途中でエラーを招く

クエリ構築用のクラスメソッドの場合には、条件分岐のelse節を省いてしまうと、メソッドチェーンの途中で予期せぬエラーを引き起こす原因になるという注意点もあります。

以下のように、先ほどのwith_colorメソッドからelse節を削除してみましょう。

class Pen < ApplicationRecord
  def self.with_color(color)
    if color.present?
      where(color: color)
    end
  end
end

引数が有効値のとき(color.present?が真になるとき)はエラーになりませんが、 以下のように引数が空文字列で条件を満たさない場合はNoMethodErrorが発生してしまいます。

test(dev):000> Pen.where(created_at: 1.year.ago..).with_color("").count
(test):0:in '<main>': undefined method 'count' for nil (NoMethodError)

これは、以下のように条件を満たさなかった場合の戻り値がnilになってしまうためです。 nilに対してcountメソッドを呼び出そうとしたため、エラーになったのです。

test(dev):000> Pen.where(created_at: 1.year.ago..).with_color("")
=> nil

クエリ構築用メソッドとしてクラスメソッドを設計するなら、 常にActiveRecord::Relationインスタンス(またはそれに準ずるオブジェクト)を返す設計にしておくことが求められます。

リレーションを返す場合はscopeに統一する

クエリ構築の一環としてリレーションを返すことを意図したメソッドは、 クラスメソッドではなくscopeでの定義に統一するのが有効です。 これは、前述したモデルのクラスメソッドに関する困難さを和らげる、シンプルなアプローチの1つです。

1つのプロジェクト内でクラスメソッドでの定義とscopeでの定義を混在させることは、 前述のクラスメソッドの注意点とscopeの注意点を足し合わせることになり、チーム開発における理解コストを上げてしまいます。 では、混在をやめてどちらに統一するのが良いかと言えば、scopeに統一することが有力な戦略です。以下にその理由を3つ述べます。

理由1: scopeとクラスメソッドでは挙動に違いがある

まず大きな違いは、nilまたはfalseを返したときの挙動です(この点については理由3で詳しく後述します)。 また、レシーバとしてモデル名を書いた際にも明確な挙動の違いが確認できます(下記「参考」を参照)。 これらに限らず、他の挙動の差異が潜んでいる可能性もあります。

このように仕様や挙動に差異がある場合、二通りのやり方が混在していると「片方で問題ない書き方がもう片方でバグになる」という厄介さを抱えることになります。また、両方の仕様・挙動に対する知識が求められるため、コードベースの理解コストも上がってしまいます。

(参考)モデルクラス名を明示的にレシーバとした場合の挙動の違い

クエリ構築を行うscopeとクラスメソッドでは、モデルクラス名を明示的にレシーバとして書いてしまうと、 前の条件を引き継ぐか否かに違いが出てしまいます。

具体的には、まず以下のように、Pen.where(color: color)とだけ記述したscopeとクラスメソッドをそれぞれ定義します。

class Pen < ApplicationRecord
  scope :with_color_s, ->(color) { Pen.where(color: color) }

  def self.with_color_m(color)
    Pen.where(color: color)
  end
end

次に、それぞれを実行してみると、以下のような違いが出ます。

test(dev):000> Pen.where(price: 80).with_color_m("red")
  Pen Load (0.2ms)  SELECT "pens".* FROM "pens" WHERE "pens"."price" = 80 AND "pens"."color" = 'red' /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x00007ad76b175648
  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">]
test(dev):000> Pen.where(price: 80).with_color_s("red")
  Pen Load (0.2ms)  SELECT "pens".* FROM "pens" WHERE "pens"."color" = 'red' /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x00007ad76ae02d08
  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:0x00007ad76ae02bc8
  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">]

クラスメソッドのwith_color_mは前の条件where(price: 80)が適切にマージされているのに対し、 scopeのwith_color_sprice: 80の条件が無視されています(where(color: color)のようにレシーバPenを消せば正しくマージされます)。

なお、この違いの確認には執筆時の最新版であるRails 8.1.2を用いました。

理由2: scopeには「リレーションを返す」という共通認識がある

クラスメソッドは、用途が極めて広い汎用的な仕組みです。そのため、メソッド名だけでは「チェーン可能なクエリメソッドなのか」が判断しづらくなります。

一方、scopeは「クエリを組み立ててActiveRecord::Relationを返すためのもの」という開発者間の共通認識があります。 また、scopeは通常のクラスメソッド定義方法とは異なるため、メソッドチェーンで利用できることも自然に受け入れられます。

以上から、scopeの方が、コードの読み書きに必要な認知的負荷が低いと言えます。

理由3: scopeでは、else節を省いた簡潔な記載が可能

RailsのAPIドキュメントによれば、 nilまたはfalseが返された場合にその時点までに構築済みのクエリ条件(allに相当)を返すというのがscopeの仕様です。

そのため、以下のscope定義にはelse節がありませんが、メソッドチェーンの途中で呼び出されてもエラーを引き起こしません。

class Pen < ApplicationRecord
  scope :with_color, ->(color) do
    if color.present?
      where(color: color)
    end
  end
end

これにより、先ほど述べたクラスメソッドが持つ「nilを返してチェーンが壊れる」問題を回避しつつ、条件付きクエリを簡潔に表現できます。

さらにRubyでは、全く同じ処理を以下の一行に書き直せます。

class Pen < ApplicationRecord
  scope :with_color, ->(color) { where(color: color) if color.present? }
end

このようにscopeを使うことで、意図が明確かつ簡潔なコードを書くことが出来ます。

まとめ

  • クエリ用途ではないクラスメソッドを定義するときには、メソッドチェーンで呼び出されることが不自然であると伝わるように、責務や名前の明確さが重要です。
  • リレーションを返すメソッドをscopeでの定義に統一することで、nil返却エラーを防いだり、コードの読み書きを容易にしたり出来ます。
0
Raw
https://www.techtips.page/en/comments/1115