Railsでモデルにクラスメソッドを定義すると、メソッドの内容に関わらず、メソッドチェーン(ActiveRecord::Relation)経由でもそのメソッドを呼び出せます。 これが便利な場合がある一方で、エラーにはならないものの、意味的に不正確なコードが書けてしまったり、気を付けないとバグの温床になったりすることもあります。 特に、リレーションを返す用途のクラスメソッドと、そうでないクラスメソッドが混在している場合に混乱が生じやすくなります。 本記事ではその仕組みと注意点、そして実務上の整理方針を解説します。 ## モデルのクラスメソッドがActiveRecord::Relation経由でも呼べる仕組み 以下のようにPenクラスにクラスメソッドを定義したとします。 ```ruby class Pen < ApplicationRecord def self.with_color(color) if color.present? where(color: color) else all end end end ``` 定義したこのメソッドは以下のように呼べます。これはRubyにおける通常のクラスメソッド呼び出しです。 ```irb test(dev):000> Pen.with_color("red") Pen Load (0.1ms) SELECT "pens".* FROM "pens" WHERE "pens"."color" = 'red' /* loading for pp */ LIMIT 11 => [#, #] ``` では、メソッドチェーンでの呼び出しはどうなっているでしょうか。 まず、`where`などのスコープメソッドを呼び出した結果は、以下で確認できる通り、 PenクラスではなくPen::ActiveRecord_Relationのインスタンスになります。 ```irb test(dev):000> Pen.where(created_at: 1.year.ago..).class => Pen::ActiveRecord_Relation ``` このリレーションインスタンスに対しても、クラスメソッドを以下のように呼び出せます。 つまり、モデルに定義したクラスメソッドは、そのモデルに対応したActiveRecord::Relationクラスのインスタンスからも呼べる仕組みになっているのです。この仕組みが、クラスメソッドをメソッドチェーンから呼べるようにしています。 ```irb 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`を間に挟んでもエラーにならずに実行できます。 ```ruby Pen.where(created_at: 1.year.ago..).export_to_csv(1, 2) ``` では`export_to_csv`メソッドは、`count`のように「条件に合ったレコードに対する処理をするメソッド」なのでしょうか? それとも、クエリ構築とは関係がない「ユーティリティメソッド」で、このメソッドチェーンで呼び出すコードは何かしらのミスの結果なのでしょうか? この例から分かるのは、モデルクラスに定義するクラスメソッドの名前はより慎重に付けなくてはならないということです。 名前だけではありません。メソッドの責務についても、クエリ用途であるか否かなど、明確さがあることが望まれます。 そのため、モデルのクラスメソッドの責務と名前に一定の規則性を持たせることも1つのアイデアです。 例えば、「`find`や`create`など、Active Recordのクエリメソッドで使われる単語から名前が始まる場合は、必ずクエリ関連の責務とする」といったルールが考えられます。 また、名前だけでなくコードドキュメントにおいても、他のメソッド以上に使い方をはっきりと示すことが望まれます。 ### クエリ構築用クラスメソッドでelse節を省くとメソッドチェーンの途中でエラーを招く クエリ構築用のクラスメソッドの場合には、条件分岐の`else`節を省いてしまうと、メソッドチェーンの途中で予期せぬエラーを引き起こす原因になるという注意点もあります。 以下のように、先ほどの`with_color`メソッドから`else`節を削除してみましょう。 ```ruby class Pen < ApplicationRecord def self.with_color(color) if color.present? where(color: color) end end end ``` 引数が有効値のとき(`color.present?`が真になるとき)はエラーになりませんが、 以下のように引数が空文字列で条件を満たさない場合は`NoMethodError`が発生してしまいます。 ```irb test(dev):000> Pen.where(created_at: 1.year.ago..).with_color("").count (test):0:in '
': undefined method 'count' for nil (NoMethodError) ``` これは、以下のように条件を満たさなかった場合の戻り値が`nil`になってしまうためです。 `nil`に対して`count`メソッドを呼び出そうとしたため、エラーになったのです。 ```irb 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とクラスメソッドをそれぞれ定義します。 ```ruby class Pen < ApplicationRecord scope :with_color_s, ->(color) { Pen.where(color: color) } def self.with_color_m(color) Pen.where(color: color) end end ``` 次に、それぞれを実行してみると、以下のような違いが出ます。 ```irb 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 => [#] 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 => [#, #] ``` クラスメソッドの`with_color_m`は前の条件`where(price: 80)`が適切にマージされているのに対し、 scopeの`with_color_s`は`price: 80`の条件が無視されています(`where(color: color)`のようにレシーバ`Pen`を消せば正しくマージされます)。 なお、この違いの確認には執筆時の最新版であるRails 8.1.2を用いました。 ### 理由2: scopeには「リレーションを返す」という共通認識がある クラスメソッドは、用途が極めて広い汎用的な仕組みです。そのため、メソッド名だけでは「チェーン可能なクエリメソッドなのか」が判断しづらくなります。 一方、scopeは「クエリを組み立ててActiveRecord::Relationを返すためのもの」という開発者間の共通認識があります。 また、scopeは通常のクラスメソッド定義方法とは異なるため、メソッドチェーンで利用できることも自然に受け入れられます。 以上から、scopeの方が、コードの読み書きに必要な認知的負荷が低いと言えます。 ### 理由3: scopeでは、else節を省いた簡潔な記載が可能 [RailsのAPIドキュメント](https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scope)によれば、 `nil`または`false`が返された場合に[その時点までに構築済みのクエリ条件(`all`に相当)](/topics/921)を返すというのがscopeの仕様です。 そのため、以下のscope定義にはelse節がありませんが、メソッドチェーンの途中で呼び出されてもエラーを引き起こしません。 ```ruby class Pen < ApplicationRecord scope :with_color, ->(color) do if color.present? where(color: color) end end end ``` これにより、先ほど述べたクラスメソッドが持つ「nilを返してチェーンが壊れる」問題を回避しつつ、条件付きクエリを簡潔に表現できます。 さらにRubyでは、全く同じ処理を以下の一行に書き直せます。 ```ruby class Pen < ApplicationRecord scope :with_color, ->(color) { where(color: color) if color.present? } end ``` このようにscopeを使うことで、意図が明確かつ簡潔なコードを書くことが出来ます。 ## まとめ - クエリ用途ではないクラスメソッドを定義するときには、メソッドチェーンで呼び出されることが不自然であると伝わるように、責務や名前の明確さが重要です。 - リレーションを返すメソッドをscopeでの定義に統一することで、`nil`返却エラーを防いだり、コードの読み書きを容易にしたり出来ます。