Railsにおけるモデルのscopeメソッド

# はじめに
先日、業務で`scope`メソッドでの実装したので、備忘録として記事にしたいと思います。

この記事では、`scope`メソッドの基本的な使い方をまとめてみました。

# モデルのscopeメソッドとは？

`scope`メソッドは、よく使用されるクエリをメソッドとして定義することができる機能です。

これにより、複雑なクエリを簡単に再利用できるようになります。

# Scopeメソッドの基本構文

`scope`メソッドの基本的な構文は以下のようになります。

```ruby
scope :scope_name, -> { query }
```

- `scope_name`: スコープの名前を指定します。
- `query`: 実行するクエリを指定します。

# Scopeメソッドの使用例

今回は、記事の一覧表示で人気の記事を取得する処理を例に見ていきます。

### scopeを使わないパターン

`scope`を使わずに人気の記事を取得する場合、以下のようにクエリを書くことになります。

```ruby
# app/models/post.rb
class Post < ApplicationRecord
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.where('likes_count > ?', 100).order(created_at: :desc).page(params[:page]).per(10)
  end
end
```

ここでは、`PostsController`の`index`アクションで、`likes_count`が100を超える記事を取得し、`created_at`の降順で並び替え、ページネーションを行っています。

### scopeを使うパターン

次に、`scope`を使って人気の記事を取得する場合です。

```ruby
# app/models/post.rb
class Post < ApplicationRecord
  scope :popular, -> { where('likes_count > ?', 100).order(created_at: :desc) }
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.popular.page(params[:page]).per(10)
  end
end
```

ここでは、`Post`モデルに`popular`というスコープを定義しています。

`PostsController`の`index`アクションでは、`Post.popular`という形で`popular`スコープを呼び出し、人気の記事を取得しています。


### 複数のコントローラーで使用できる例

`scope`を使うことで、複数のコントローラーで同じクエリを再利用できます。

```ruby
# app/models/post.rb
class Post < ApplicationRecord
  scope :popular, -> { where('likes_count > ?', 100).order(created_at: :desc) }
end



# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.popular.page(params[:page]).per(10)
  end
end

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    @popular_posts = Post.popular.limit(5)
  end
end

# app/controllers/admin/posts_controller.rb
class Admin::PostsController < ApplicationController
  def index
    @posts = Post.popular.page(params[:page]).per(20)
  end
end
```

ここでは、`PostsController`、`HomeController`、`Admin::PostsController`の3つのコントローラーで`popular`スコープを使用しています。

`Post.popular`という形で`popular`スコープを呼び出すだけで、人気の記事を取得できます。

# 返り値について（ActiveRecord::Relation）

`scope`メソッドは、`ActiveRecord::Relation`オブジェクトを返します。

これによってクエリメソッドをチェーンして使用できるメリットがあります。

```ruby
# app/models/post.rb
class Post < ApplicationRecord
  scope :popular, -> { where('likes_count > ?', 100).order(created_at: :desc) }
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.popular.where(category: 'Ruby').page(params[:page]).per(10)
  end
end
```

`popular`スコープの後に`where`メソッドをチェーンして使用しています。

これにより、人気の記事の中からカテゴリが「Ruby」の記事のみを取得することができます。

`ActiveRecord::Relation`オブジェクトを返すことで、このようにクエリメソッドを柔軟に組み合わせることができます。

# nilの場合について（scopeとclassメソッドの違い）

`scope`メソッドと通常のクラスメソッドの大きな違いの一つは、nilを返す場合の動作です。

```ruby
# app/models/post.rb
class Post < ApplicationRecord
  # scope
  scope :popular, -> { where('likes_count > ?', 100).order(created_at: :desc) }

  # classメソッド
  def self.popular_class_method
    where('likes_count > ?', 100).order(created_at: :desc)
  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    # scopeを使用した場合
    @posts = Post.popular.where(category: 'Ruby').page(params[:page]).per(10)

    # classメソッドを使用した場合
    @posts_class_method = Post.popular_class_method.where(category: 'Python').page(params[:page]).per(10)
  end
end
```

ここでは、popularスコープとpopular_class_methodクラスメソッドを定義しています。

|  | popular<br>スコープ | popular_class_method<br>クラスメソッド |
|:-:|:-:|:-:|
| 条件に一致する記事がない場合の戻り値  | 空のActiveRecord::Relationオブジェクト  | nil  |
| コントローラー内の変数への代入  | @posts変数には空のオブジェクトが代入される  | @posts_class_method変数にはnilが代入される  |
| ビュー表示 | エラーなく空のビューが表示される | NoMethodErrorが発生する |

`scope`メソッドを使用することで、常に`ActiveRecord::Relation`オブジェクトを返すことができます。
これによって、`scope`メソッドで定義したスコープに対して、メソッドチェーンを常に使えるのがメリットです。

また、`ActiveRecord::Relation`オブジェクトを返すことで、意図しない`NoMethodError`などのエラーを防ぐことができます。


# 参考文献

https://railsguides.jp/active_record_querying.html#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97

https://railsguides.jp/active_record_querying.html#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%83%81%E3%82%A7%E3%82%A4%E3%83%B3%E3%82%92%E7%90%86%E8%A7%A3%E3%81%99%E3%82%8B

https://zenn.dev/yusuke_docha/articles/ca0637ccc8d01f