😮

【Rails】`scope` を定義してクエリを使い回せるんだ〜

2022/07/24に公開

こんにちは。Webエンジニアとなって3週間経ったオクトと申します。

scope機能について全く知らない方や何となく知ってはいたけど、具体的にどうやって使っていくか不安だな〜っていう方向け(私も含めて)にシェアしていこうと思います。

ご指摘箇所がございましたら、ご教授いただけますと幸いです。

前提

Ruby 3.1.1
Rails 6.1.4.6

scope機能ってなんすか??

そもそもscope機能とはなんだい?

scope機能とは、特定の条件式に名前を付けてあげて、それを呼んであげるとその条件式を実行してくれるものです。メソッドのように扱えるということですね。』

ふむふむ。ちょっと気になるな〜。具体的にはどんな風に書くん?

『以下のように、書いてあげることで定義することができます。』

class Post < ApplicationRecord
  # scope :<スコープの名前>, -> { <条件式> }
  scope :find_sports_of_category, -> { where(category: "スポーツ") }
end

enum category: {
  "経済": 1,
  "エンタメ": 2,
  "グルメ": 3,
  "スポーツ": 4
}

-----

# posts_controller.rb
def index
  # モデル名.スコープ名
  @posts = Post.find_sports_of_category
end

#=> Post Load (1.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."category" = $1  [["category", 4]]

おお〜、こういう風に書くのね。scopeを使えば、繰り返し使う条件には有効だし、コードも短くなるからめっちゃ便利じゃん。
ということは、scopeの第一引数には、シンボルでの名前を指定して、第二引数には条件式を持つlamdaを渡しているんだね。


『しかし、scope機能はこれだけじゃなくて、引数も渡すことができるんですよ!』

ほうほう。詳しく教えてくれ。

『以下のように引数を定義すると呼び出し側で仮引数として渡すことができるんですよ!』

class Post < ApplicationRecord
  # scope :<スコープの名前>, -> (<引数>) { <条件式> }
  scope :find_category, -> (category) { where(category: category) }
end

-----

def index
  @posts = Post.find_category('スポーツ')
end

#=> Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."category" = $1  [["category", 4]]

おお〜!クラスメソッドのイメージを持っておくと扱いやすいね。

『そうですね。呼び出すときはクラスメソッドとして呼び出すことができます。
Railsガイドにおいても、引数を使ったスコープ機能はクラスメソッドの機能を複製したにすぎないと言っているので、クラスメソッドとスコープ機能を使い分けるのはケースバイケースということになります。
例えば、スコープ機能は1行で記述をするので、この1行が長くなる場合にはクラスメソッドの方が可読性が上がる、ということになります。』


『さらにさらになんとif文を使って条件分岐を行うと、
trueの場合は、そのまま条件式が処理されて
falseの場合は、 allメソッドを呼び出して実行してくれるんです!』

# ===== `true`の場合 =====
class Post < ApplicationRecord
  # scope :<スコープの名前>, -> (<引数>) { <条件式> if <式> }
  scope :find_category, -> (category) { where(category: category) if category == 'スポーツ' }
end

-----

def index
  @posts = Post.find_category('スポーツ')
end

# => Post Load (0.6ms)  SELECT "posts".* FROM "posts" WHERE "posts"."category" = $1  [["category", 4]]
# ===== `false`の場合 =====
class Post < ApplicationRecord
  # scope :<スコープの名前>, -> (<引数>) { <条件式> if <式> }
  scope :find_category, -> (category) { where(category: category) if category != 'スポーツ' }
end

-----

def index
  @posts = Post.find_category('スポーツ')
end

# => Post Load (0.9ms)  SELECT "posts".* FROM "posts"

これもめちゃめちゃ便利やん。メソッド名の適切さは置いておこうか、、、


『メソッドチェーンを利用して他のメソッドをつなげることもできます!
メソッドチェーンについて詳しく知りたい方はこちらの記事Railsガイドを読むと理解が深まります。
scopeメソッドはActiveRecord::Relationオブジェクトが返ってくることを覚えておきましょう〜』

class Post < ApplicationRecord
  # scope :<スコープの名前>, -> (<引数>) { <条件式> }
  scope :find_category, -> (category) { where(category: category) }
end

-----

def index
  # モデル.スコープ.メソッド
  @posts = Post.find_category('スポーツ').order(created_at: :desc)
end

#=> Post Load (3.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."category" = $1 ORDER BY "posts"."created_at" DESC  [["category", 4]]

おいおい、カスタマイズし放題じゃないか。。。

『しかし、注意点としてscope内がnilの場合でも allメソッドを呼び出してActiveRecord::Relationオブジェクトを返すので、条件式は何が返るのかを意識しないと思わぬ処理につながってしまいます。』

class Post < ApplicationRecord
  # scope :<スコープの名前>, -> { <条件式> }
  scope :find_sports_of_category, -> { nil }
end

-----

def index
  @posts = Post.find_sports_of_category
end

# indexアクションでスポーツのカテゴリーで絞り込みをしていると思ったら、全てのポストが返ってきた
#=> Post Load (0.9ms)  SELECT "posts".* FROM "posts"

うわわ、それはしっかり注意しないとだね。

複数のscopeメソッドを繋げる事もでき、かつ1回のクエリ発行で済むためパフォーマンスが良くなります。』

class Post < ApplicationRecord
  scope :find_category, -> (category) { where(category: category) }
  scope :recent, -> { order(created_at: :desc) }
end

-----

def index
  @posts = Post.find_category('スポーツ').recent
end

#=> Post Load (1.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."category" = $1 ORDER BY "posts"."created_at" DESC  [["category", 4]]

いかがだったでしょうか。
使い方次第では、とても便利なメソッドかと思います!

最後までお読みいただきありがとうございます!

参考

Discussion