Railsでスコープを活用してクエリを簡潔に記述する方法

# Railsでスコープを活用してクエリを簡潔に記述する方法

## はじめに

前回の記事では、`controller`ファイル内で**平均難易度が1から3の間**で投稿数の多い本を8件取得するコードを作成しました。詳細はこちらの記事をご参照ください。

https://qiita.com/tttttaku/items/e88490ee2470ff9b3cba

しかし、今回は**平均難易度が2から4**や**3から5**の場合も同様にデータを取得したいと考えています。愚直に実装すると、同じようなコードが繰り返されてしまい、コードが冗長になります。

```ruby
@books_dif_low = Book
  .joins(:posts)
  .select('books.*, AVG(posts.difficulty) AS average_difficulty, COUNT(posts.id) AS posts_count')
  .group('books.id')
  .having('AVG(posts.difficulty) BETWEEN ? AND ?', 1, 3)
  .order('posts_count DESC')
  .limit(8)

@books_dif_mid = Book
  .joins(:posts)
  .select('books.*, AVG(posts.difficulty) AS average_difficulty, COUNT(posts.id) AS posts_count')
  .group('books.id')
  .having('AVG(posts.difficulty) BETWEEN ? AND ?', 2, 4)
  .order('posts_count DESC')
  .limit(8)

@books_dif_high = Book
  .joins(:posts)
  .select('books.*, AVG(posts.difficulty) AS average_difficulty, COUNT(posts.id) AS posts_count')
  .group('books.id')
  .having('AVG(posts.difficulty) BETWEEN ? AND ?', 3, 5)
  .order('posts_count DESC')
  .limit(8)
```

そこで、**スコープ**を活用することで、コードを簡潔かつ再利用可能にする方法を紹介します。

## スコープの使い方

### スコープとは？

**Scope（スコープ）は、RailsのActiveRecord**モデル内で定義される、再利用可能なクエリの定義方法です。スコープを使用することで、複雑なクエリをシンプルかつ読みやすく管理できます。

### なぜスコープを使うのか？

1. **再利用性の向上**
    
    同じ条件やロジックを複数の場所で使用する際に、一度スコープとして定義しておけば簡単に呼び出せます。
    
2. **コードの可読性向上**
    
    複雑なクエリを名前付きのスコープに分割することで、コード全体の理解が容易になります。
    
3. **チェーン可能**
    
    複数のスコープを組み合わせて柔軟なクエリを構築できます。
    

### スコープの書き方

基本的なスコープの定義方法は以下の通りです。

```ruby
scope :scope_name, ->(parameters) { where(condition: value) }
```

次に、具体的なスコープの定義方法を見ていきましょう。

## スコープを使った記述

### 1. `with_average_difficulty_between`

```ruby
scope :with_average_difficulty_between, ->(min, max) {
  joins(:posts)
    .select('books.*, AVG(posts.difficulty) AS average_difficulty, COUNT(posts.id) AS posts_count')
    .group('books.id')
    .having('AVG(posts.difficulty) BETWEEN ? AND ?', min, max)
}
```

### 役割と機能

このスコープは、**平均難易度が指定された範囲（`min`から`max`）にある本**を取得します。また、各本の**平均難易度**と**投稿数**も計算・取得します。

### 2. `ordered_by_posts_count`

```ruby
scope :ordered_by_posts_count, -> {
  order('posts_count DESC')
}
```

### 役割と機能

このスコープは、**投稿数（`posts_count`）が多い順**に本を並べ替えます。

### 3. `top_ranked`

```ruby
scope :top_ranked, ->(limit = 8) {
  limit(limit)
}
```

### 役割と機能

このスコープは、**取得する本の数を制限**します。デフォルトでは8件の本を取得しますが、引数で指定することも可能です。

## Scopeを組み合わせたクエリの実行

定義した3つのスコープをチェーン（連結）することで、目的のデータを簡潔に取得できます。以下に具体例を示します。

```ruby
# app/models/book.rb
class Book < ApplicationRecord
  has_many :posts

  scope :with_average_difficulty_between, ->(min, max) {
    joins(:posts)
      .select('books.*, AVG(posts.difficulty) AS average_difficulty, COUNT(posts.id) AS posts_count')
      .group('books.id')
      .having('AVG(posts.difficulty) BETWEEN ? AND ?', min, max)
  }

  scope :ordered_by_posts_count, -> {
    order('posts_count DESC')
  }

  scope :top_ranked, ->(limit = 8) {
    limit(limit)
  }
end
```

これらのスコープを使用して、平均難易度と投稿数に基づいた本を簡単に取得できます。

```ruby
# コントローラー内の例
@books_dif_low = Book
  .with_average_difficulty_between(1, 3)
  .ordered_by_posts_count
  .top_ranked

@books_dif_mid = Book
  .with_average_difficulty_between(2, 4)
  .ordered_by_posts_count
  .top_ranked

@books_dif_high = Book
  .with_average_difficulty_between(3, 5)
  .ordered_by_posts_count
  .top_ranked
```

このようにスコープを活用することで、クエリが簡潔になり、メンテナンス性も向上します。

## 終わりに

スコープを活用することで、**冗長なコードを避ける**ことができ、**コードの可読性と再利用性**が向上します。また、スコープを定義する際にも**SQLの基礎知識**が大切になると感じました。