scopeメソッドとは


## scopeとは

scopeとは、データベースからレコードを取り出すロジックをカプセル化するメソッド。

基本構文は、第一引数に条件式を呼び出すためのスコープ名、第二引数に条件式を記述します。

```ruby
class モデル名 < ApplicationRecord
  scope :スコープ名, -> { 条件式 }
end
```

また、条件式に引数を渡すこともできる。

```ruby
class モデル名 < ApplicationRecord
  scope :スコープ名, -> (引数){ 条件式 }
end
```

## メリット

scopeメソッドを使用するメリットは、以下の3つ。

①モデルとコントローラに正しく役割を分担できる

②可読性が上がる

③DRY(Don't repeat yourself)に則ったコードを書ける

例）booksテーブルからタイトルが2000円以下で、ジャンルが食のレコードを取り出す場合。

| id | title | genre | price | out_of_stock |
| --- | --- | --- | --- | --- |
| 1 | 簡単本格中華レシピ | 食 | 2100 | false |
| 2 | 週刊山男 | アクティビティ | 1500 | false |
| 3 | ワールドフットボール | スポーツ | 2300 | true |
| 4 | 朝ごはんのススメ | 食 | 1500 | false |
| 5 | ベースボールマガジン | スポーツ | 2800 | true |

**scopeを使わずに記述した場合**

```ruby:books_controller.rb
book = Book.where(genre: "食").where("price <= ?", 2000)

#取得されるレコード
=> [
  id: 4,
  title: "朝ごはんのススメ",
  genre: "食",
  price: 1500>]
```

ジャンルが”食”で金額が2000円以下のレコードが取得された。

**scopeでクエリメソッドをBookモデルに定義して記述した場合**

```ruby:book.rb
#引数で指定したジャンル別で検索する
scope :genre_search, ->(genre) { where(genre: genre) }

#引数で指定した金額以下の本を検索する
scope :maximum_price_search, ->(price) { where("price <= ?", price) }
```

```ruby:books_controller.rb
book = Book.genre_search("食").maximum_price_search(2000)

#scopeを使用しない記述と同じように取得したいレコードを取り出せる。
=> [
  id: 4,
  title: "朝ごはんのススメ",
  genre: "食",
  price: 1500>]
```


メリット1：**モデルとコントローラに正しく役割を分担できる**

scopeを使わずに記述した場合では、booksコントローラに直接クエリメソッドを記述してレコードを取り出している。しかし、Railsではデータベースとやりとりしてレコードを取り出す役割はモデルが担っているためwhere文のような検索ロジックはモデルに記載するのが正しい。

メリット2：**可読性が上がる**

scopeを使わずに記述した場合では、where文を連結させて取り出したいレコードを指定している。それに対し、scopeを使用した場合ではで検索ロジックに名前をつけてモデルにカプセル化し、コントローラで取り出して使っている。そのため、何をしているのかわかりやすくなる且つ簡潔に書ける。

メリット3：**DRYに則ったコードを書ける**

何度も使用する検索ロジックに名前をつけてscope化しておくことで、何度も同じコードを書く必要がなくなる。また、変更する場合にもscope内を変更だけで同じロジックを使っている箇所全てを変更できる。

## scopeメソッドで条件文を使う

以下のようにscopeメソッドの中にifを使って処理を分岐することができる。

ここでは例として引数のcountが5未満ならクエリを実行するというメソッドを定義。
booksテーブルの中でout_of_stockカラムがfalseなレコードは3つあるので以下のように取り出すことができます。

```ruby
scope :available, -> (count) { where(out_of_stock: false).limit(count) if count < 5 }
```

```ruby
Book.available(3)

#実行結果
=> [
  id: 1,
  title: "簡単本格中華レシピ",
  genre: "食",
  price: 2100,
  out_of_stock: false>,

  id: 2,
  title: "週刊山男",
  genre: "アクティビティ",
  price: 1500,
  out_of_stock: false>,

  id: 4,
  title: "朝ごはんのススメ",
  genre: "食",
  price: 1500,
  out_of_stock: false>]
```

次に、条件文の結果がfalseの場合の返り値を確認するために以下のように引数に渡すcountの値を変更。

```ruby
Book.available(10)

SELECT "books".* FROM "books"

#実行結果
=> [
  id: 1,
  title: "簡単本格中華レシピ",
  genre: "食",
  price: 2100,
  out_of_stock: false>,

  id: 2,
  title: "週刊山男",
  genre: "アクティビティ",
  price: 1500,
  out_of_stock: false>,

  id: 3,
  title: "ワールドフットボール",
  genre: "スポーツ",
  price: 2300,
  out_of_stock: true>,

  id: 4,
  title: "朝ごはんのススメ",
  genre: "食",
  price: 1500,
  out_of_stock: false>,

  id: 5,
  title: "ベースボールマガジン",
  genre: "スポーツ",
  price: 2800,
  out_of_stock: true>]
```

発行されたSQL文は`SELECT "books".* FROM "books"` となっており、scopeメソッドの条件文に対する結果がfalseの場合にはBook.allと同じ結果が返される。また、以下のように条件文がnilを返す場合でも同じ結果になリます。

```ruby
scope :available, -> { nil } #availableの条件文をnilに変更
```

```ruby
Book.available

SELECT "books".* FROM "books"

#実行結果
=> [
  id: 1,
  title: "簡単本格中華レシピ",
  genre: "食",
  price: 2100,
  out_of_stock: false>,

  id: 2,
  title: "週刊山男",
  genre: "アクティビティ",
  price: 1500,
  out_of_stock: false>,

  id: 3,
  title: "ワールドフットボール",
  genre: "スポーツ",
  price: 2300,
  out_of_stock: true>,

  id: 4,
  title: "朝ごはんのススメ",
  genre: "食",
  price: 1500,
  out_of_stock: false>,

  id: 5,
  title: "ベースボールマガジン",
  genre: "スポーツ",
  price: 2800,
  out_of_stock: true>]
```

## クラスメソッドとscopeの違い

以下の例のようにscopeはクラスメソッドのように振る舞います。しかし、条件文の結果がnilの場合の返り値がscopeメソッドとクラスメソッドで違うため注意が必要です。

```ruby
#クラスメソッドで定義
def self.genre_search(genre)
    where(genre: genre)
end

def self.maximum_price_search(price)
	where("price <= ?", price)
end

#scopeメソッドで定義
scope :genre_search, ->(genre) { where(genre: genre) }
scope :maximum_price_search, ->(price) { where("price <= ?", price) }
```

```ruby
Book.genre_search("食").maximum_price_search(2000)

#クラスメソッドをで定義した場合と、scopeメソッドで定義した場合で出力される結果は同じ
=> [
  id: 4,
  title: "朝ごはんのススメ",
  genre: "食",
  price: 1500>]
```

### クラスメソッドの結果がnilの場合

scopeメソッドではBook.allの結果が返されていたが、クラスメソッドでが以下のように定義した条件文がfalseの場合にはnilが返ってくる。

```ruby
def self.available(count)
  if count < 5
    where(out_of_stock: false).limit(count)
  end
end
```

```ruby
Book.available(10)

#実行結果
=> nil
```

また、メソッドチェーンを使ってクラスメソッドとselectメソッドを繋げて記述した場合には、nilに対してselectを繋げることになるため以下のようにエラーになる。

```ruby
Book.available(10).select(:id, :title)

#実行結果
`select' called for nil:NilClass (NoMethodError)
```

## 参考にしたサイト

- **Railsガイド**

https://railsguides.jp/active_record_querying.html#スコープ](https://railsguides.jp/active_record_querying.html#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97)

- **【Rails】 モデルのスコープ機能(scope)の使い方を１から理解する**

https://pikawaka.com/rails/scope

- **Railsでよく利用する、Scopeの使い方。**

https://qiita.com/ngron/items/14a39ce62c9d30bf3ac3