Ruby
Rails

便利なscope

はじめに
前回の記事でenum素敵ってなりましたね。じゃあscopeも素敵ってなっときましょう。

scopeとは

scope?グローバル変数とかのこと?とか最初は勘違いしてました。違います。
こいつは、SQL文をメソッドにしてくれる良いやつなんです。
では例を用いて説明しましょう。

scopeを書かない場合

以下の様にな探索をかける場合、この様なuserの探索が他の場面でも必要になった時に管理に困ります。やっぱり年齢は20歳以上で探索しようだの、やっぱり女性に絞ろうだのいった時に修正箇所が広がり大変です。
なのでscopeを用いてメソッドとし、カプセル化しましょう。オブジェクティブに生きていくと幸せなのです。

users_controller.rb
class UsersController < ApplicationController
  def index
    @user_index = User.where(status: :normal)
                        .where(gender: :male)
                        .where('age >= 18')

  end
end

scopeでmodelにまとめる。

さっきのsql文をmodelにまとめると下記の様になります。

user.rb
class User < ApplicationRecord

  enum status: {
    normal:  0, #通常会員
    premium: 1, #プレミアム会員
  }
  enum gender: {
    male:    0, #男性
    female:  1, #女性
  }

  scope :status_normal, -> { where(status: :normal) }
  scope :male,          -> { where(gender: :male) }
  scope :adult,         -> { where('age >= 18') }

end

上記が基本のscopeの構文です。scope スコープ名 処理となってます。処理はラムダ式で書いてます。do~endでもかけます。(ラムダ式の方が綺麗に見えるンゴ!)
これでコントローラにてメソッドが使えます。

users_controller.rb
class UsersController < ApplicationController
  def index
    @user_index = User.status_normal.male.adult

  end
end

ここで一つ、クラスメソッドでもよくね?という疑問。
確かに、User.rbで上記のクエリを発行する様なメソッドを定義しても良さそうです。
しかし、スコープとクラスメソッドの違いはメソッドチェーンの点で利があることです。
scopeでは条件がnilの場合.allを返してくれます。

例えば引数を与えて、それを元に条件式を用いる場合、

User.rb
scope :age_younger, ->(age) { where("age < ?", age) if age.present? }
User.rb
class << self
  def self.age_younger(age)
    where("age < ?", age) if age.present?
  end
end

上記では条件式がfalseの場合、scopeは.allを返してくれますが、クラスメソッドはnilを返すので安易にメソッドチェーンにできません。

例えば、「20歳以上、男性」の検索の時は困りませんが、「年齢指定なし、男性」の検索の時、クラスメソッドだとメソッドチェーンしていた場合、nilが返ってしまいます。一方、scopeは年齢指定なしでも.allが返ってくるので男性で検索がかかります。
これがscopeの利点です。
クエリがにnilになる可能性がある場合がscopeを使いましょう。確実にクエリが存在する場合はクラスメソッドでも大丈夫です。

最後に、下記のメソッドチェーンをコントローラに書いとくのは再利用もできないし、管理がしにくくなるので、modelでメソッドにまとめます。

@user_index = User.status_normal.male.adult

user.rb
class User < ApplicationRecord
#ここまでは略します

  scope :status_normal, -> { where(status: :normal) }
  scope :male,          -> { where(gender: :male) }
  scope :adult,         -> { where('age >= 18') }

    class << self
    #通常会員の成人男性のクエリ発行
    def noraml_adult_male
      status_normal.male.adult
    end
  end
end
users_controller.rb
class UsersController < ApplicationController
  def index
    @user_index = User.normal_adult_male                 
  end
end

めちゃんこスッキリですね!この様にオブジェクティブ指向なコーディングが自然にできる様になりたいものです。

ちょっと待ったあああああああああああああああああ

すみません。ご指摘が入りまして、
以下のscope(status_normal,male)はenumで既にwhere句が生成されているのでいらないですね。

user.rb
class User < ApplicationRecord

  enum status: {
    normal:  0, #通常会員
    premium: 1, #プレミアム会員
  }
  #User.normalで`where status = 0`
  enum gender: {
    male:    0, #男性
    female:  1, #女性
  }
  #User.maleで`where status = 0

  scope :adult,         -> { where('age >= 18') }

    class << self
    #通常会員の成人男性のクエリ発行
    def noraml_adult_male
      normal.male.adult
    end
  end


end

まとめ

明日はまだなに書くか決めてませんが、頑張ります。

参考にしたの

Rails: スコープをモデルの外でチェインするのはやめておけ(翻訳)
https://techracho.bpsinc.jp/hachi8833/2018_03_19/53537

Railsでよく利用する、Scopeの使い方。
https://qiita.com/ngron/items/14a39ce62c9d30bf3ac3

Active Record クエリインターフェイス
https://railsguides.jp/active_record_querying.html

【Rails】 scope は 常に ActiveRecord_Relation を返す素晴らしいやつ
http://bamboo-yujiro.hatenablog.com/entry/2017/07/21/011138