今回はモデルにスコープで定義する場合とクラスメソッドで定義する場合の違いについてまとめてみました。最近まではどちらも挙動としては変わらない(むしろショートハンドで書けるスコープの方が優秀)と認識していましたが、ある条件下では挙動に違いがあったため備忘録としてまとめています。
【前提】スコープの基礎
モデルのスコープとは、ある条件式に名前を付けてモデルに定義することで、メソッドのように呼び出せる機能のことです。
例えば「年齢が10歳以上の人を取得する」、「性別が男性の人を取得する」という2つの検索条件をUserモデルにそれぞれ定義すると仮定します。
スコープで定義する場合
「年齢が10歳以上の人を取得する」という条件式をover_tenという名前で、「性別が男性の人を取得する」という条件式をmaleという名前で定義することで以下のような記述になります。
class User < ApplicationRecord
scope :over_ten, -> { where("age > ?", 10) }
scope :male, -> { where(gender: "男性") }
end
このようにスコープを定義したことで、User.over_tenやUser.maleと記載するだけで、それぞれの検索条件に合致したユーザーを取得することが可能になります。
User.over_ten
=>全ユーザーの中で年齢が10歳以上の人を取得する
User.male
=>全ユーザーの中で性別が男性の人を取得する
またスコープはメソッドチェーンを使うことができるため、User.over_ten.maleのようにすることで「全ユーザーの中で年齢が10歳以上の男性の人」に合致するユーザーを取得することも可能です。
User.over_ten.male
=>全ユーザーの中で年齢が10歳以上の男性の人を取得
このようにモデルのスコープを利用することで、クエリの再利用性が上がること、クエリに命名できることによる可読性の向上が期待できます。
クラスメソッドで定義する場合
先ほどスコープで定義した「年齢が10歳以上の人を取得する」、「性別が男性の人を取得する」という2つの検索条件は、以下のようにクラスメソッドで定義することも可能です。
class User < ApplicationRecord
def self.over_ten
where("age > ?", 10)
end
def self.male
where(gender: "男性")
end
end
こちらも同様にUser.over_tenやUser.maleと記載するだけで、それぞれの検索条件に合致したユーザーを取得することが可能になります。
User.over_ten
=>全ユーザーの中で年齢が10歳以上の人を取得する
User.male
=>全ユーザーの中で性別が男性の人を取得する
ここまでの使い方では、スコープで定義した場合とクラスメソッドで定義した場合の違いはないため好きな方を使っていただいて構いません。(ただ個人的には可読性の面からスコープで書く方が好きです)
ではどのような場合にスコープとクラスメソッドで違いが出てくるのでしょうか。
それは検索条件の返り値がnilやfalseの場合になります。
スコープとクラスメソッドの違い
検索条件の返り値がnilやfalseの場合、クラスメソッドで定義した場合はnilが返る一方で、スコープで定義した場合は全件取得(allメソッドの実行)になります。
先ほどの「年齢が10歳以上の人を取得する」、「性別が男性の人を取得する」という2つの検索条件で見てみましょう。
*検索条件に合致するユーザーがいないと仮定します。
スコープの場合
class User < ApplicationRecord
scope :over_ten, -> { where("age > ?", 10) }
scope :male, -> { where(gender: "男性") }
end
User.over_ten
=>ユーザーが全件返却される
User.male
=>ユーザーが全件返却される
クラスメソッドの場合
class User < ApplicationRecord
def self.over_ten
where("age > ?", 10)
end
def self.male
where(gender: "男性")
end
end
User.over_ten
=>nil
User.male
=>nil
このことからクラスメソッドでメソッドチェーンを行う場合はNoMethodErrorが発生する可能性があることを考慮しなければなりません。
User.over_ten.male
=>10歳以上のユーザーがいない場合、NoMethodErrorが発生する可能性がある
引数が渡される場合
検索条件に引数が渡される場合についても触れておきます。
検索条件に引数が渡される場合、スコープとクラスメソッドでは以下のように記述することができます。(「性別が男性の人を引数に指定された人数分取得する」という検索条件に変更しています。)
class User < ApplicationRecord
scope :over_ten, -> { where("age > ?", 10) }
scope :male, -> (count){ where(gender: "男性").limit(count) }
end
class User < ApplicationRecord
def self.over_ten
where("age > ?", 10)
end
def self.male(count)
where(gender: "男性").limit(count)
end
end
しかしRailsガイドによると、スコープの引数はクラスメソッドの機能を複製したものであるため、スコープに引数が渡される場合はクラスメソッドの使用が推奨されています。
ただし、スコープに引数を渡す機能は、クラスメソッドによって提供される機能を単に複製したものです。
参照: Railsガイド 引数を渡す
*前述しましたが、クラスメソッドでメソッドチェーンを行う場合はNoMethodErrorが発生する可能性があるので注意が必要です。
まとめ
- 検索条件の返り値がnilやfalseの場合、クラスメソッドで定義した場合はnilが返る
- 検索条件の返り値がnilやfalseの場合、スコープで定義した場合は全件取得(allメソッドの実行)になる
- クラスメソッドでメソッドチェーンを行う場合、NoMethodErrorが発生する可能性があることを考慮しなければならない
- 検索条件に引数が渡される場合は、クラスメソッドの使用が推奨される(NoMethodErrorには注意)
参考
今回はモデルにスコープで定義する場合とクラスメソッドで定義する場合の違いについてまとめました。僕自身スコープ一辺倒で使っていましたが、今後はクラスメソッドとの使い分けを意識しつつコードを書いていこうと思います。