Railsで多対多の関係を扱う - Part 4: Railsの多対多関係におけるページネーションと順序指定の問題解決

## 前回までの記事
https://qiita.com/KM9973/items/9072aa1f0c43284e536d

https://qiita.com/KM9973/items/9ac44032e1918bbc8b35

https://qiita.com/KM9973/items/85c9861623dc18faeecf

## はじめに
多対多の関係を持つモデル間でデータを取得し、それを一定の順序でページネーションする場合、特にRailsでは複雑な問題に直面することがあります。本記事では、具体的な問題点とその解決策を、実際のコードを用いて詳しく解説します。

## 問題の発生
例として、`Service`と`Category`が多対多の関係にあり、関連する`Category`の名前で`Service`をソートしたい場合を考えます。以下のようなコードでデータを取得しています。

```ruby
@services = current_company.services
                           .includes(:categories)
                           .order("categories.name #{sort_order}, services.name #{sort_order}")
                           .page(params[:page]).per(10)
```

しかし、このコードでは期待される10個ではなく、ページあたり9個や8個のレコードが表示され、ページネーションリンクも適切に表示されない問題が発生しています。

## 原因の解析
この問題の主な原因は、`order`句で`categories.name`を使用しているため、関連付けられたカテゴリごとに`services`が異なる順番で繰り返し表示されるためです。これにより、ページあたりのサービス数が不一致になり、ページネーションがうまく機能しません。

## 解決策
1. **DISTINCTの使用**
   最も簡単な解決策は、クエリに`distinct`を追加することです。これにより重複する`services`レコードを排除できますが、ソートの結果が期待と異なることがあります。

   ```ruby
   @services = current_company.services
                              .includes(:categories)
                              .order("categories.name #{sort_order}, services.name #{sort_order}")
                              .distinct
                              .page(params[:page]).per(10)
   ```

2. **joinsを使用**
   `joins`を使用すると、関連するカテゴリに基づいてサービスをソートする場合に特に有効です。`joins`は`includes`と異なり、Eager Loadingは行われませんが、ソートやフィルタリングには最適です。

   ```ruby
   @services = current_company.services
                              .joins(:categories)
                              .order("categories.name #{sort_order}, services.name #{sort_order}")
                              .distinct
                              .page(params[:page]).per(10)
   ```

3. **サブクエリの使用**
   より複雑なケースでは、SQLのサブクエリを使ってデータを先にフィルタリングしてからページネーションする方法が有効です。

   ```ruby
   @services = current_company.services
                              .select('services.*, MIN(categories.name) as first_category_name')
                              .joins(:categories)
                              .group('services.id')
                              .order("first_category_name #{sort_order}, services.name #{sort_order}")
                              .page(params[:page]).per(10)
   ```

   このクエリでは、各サービスについて最初のカテゴリ名を取得し、それに基づいてソートを行います。これにより、各ページに一貫性のある順序でサービスが表示され、ページネーションも正しく機能します。

## サブクエリの解説
サブクエリを使ったソリューションで問題が解決できたとのこと、良かったですね！それでは、そのクエリの意味と意図について詳しく解説しますね。

### サブクエリの使用目的

このクエリの主な目的は、複数のカテゴリに関連する各サービスを一意に並べ替えて表示し、ページネーションを正しく機能させることです。特に、`services` テーブルと `categories` テーブルが多対多の関係にあるため、同じサービスが異なるカテゴリ名で複数回リストされるのを防ぎます。

### クエリの解析

ここで使用されているクエリは次のようになっています。

```ruby
@services = current_company.services
                           .select('services.*, MIN(categories.name) as first_category_name')
                           .joins(:categories)
                           .group('services.id')
                           .order("first_category_name #{sort_order}, services.name #{sort_order}")
                           .page(params[:page]).per(10)
```

各部分の役割を詳しく見ていきましょう。

1. **select('services.*, MIN(categories.name) as first_category_name')**
   - この部分は、`services` テーブルのすべてのカラムと、関連する `categories` の中で辞書順に最も早い（最小の）カテゴリ名を選択します。ここで `MIN(categories.name)` を使うことで、各サービスに対して一意のカテゴリ名を確保し、その名前でソートできるようになります。

2. **joins(:categories)**
   - `services` と `categories` テーブルを内部結合します。これにより、サービスに関連するカテゴリのデータが利用できるようになります。

3. **group('services.id')**
   - `services.id` でグループ化することにより、各サービスごとに集約操作（この場合は `MIN`）を適用します。これがないと、`MIN` 関数を使用することができません。

4. **order("first_category_name #{sort_order}, services.name #{sort_order}")**
   - 選択されたカテゴリ名（`first_category_name`）とサービス名（`services.name`）に基づいて、結果を並べ替えます。`sort_order` は動的に 'ASC' または 'DESC' を指定することができ、ソートの順序を制御します。

5. **page(params[:page]).per(10)**
   - 結果をページネーションするためのメソッドです。`page` メソッドで現在のページを設定し、`per` メソッドで1ページあたりのアイテム数を指定しています。


このクエリは、多対多の関係にあるデータを効率的に処理し、特定の順序でソートした結果を一貫して表示するために設計されています。さらに、ページネーションを適切に機能させるために、サービスごとにカテゴリ名を一意にする工夫がなされています。このようなアプローチは、複雑なデータモデルを扱う際に非常に役立ちます。