前回までの記事
はじめに
多対多の関係を持つモデル間でデータを取得し、それを一定の順序でページネーションする場合、特にRailsでは複雑な問題に直面することがあります。本記事では、具体的な問題点とその解決策を、実際のコードを用いて詳しく解説します。
問題の発生
例として、Service
とCategory
が多対多の関係にあり、関連するCategory
の名前でService
をソートしたい場合を考えます。以下のようなコードでデータを取得しています。
@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
が異なる順番で繰り返し表示されるためです。これにより、ページあたりのサービス数が不一致になり、ページネーションがうまく機能しません。
解決策
-
DISTINCTの使用
最も簡単な解決策は、クエリにdistinct
を追加することです。これにより重複するservices
レコードを排除できますが、ソートの結果が期待と異なることがあります。@services = current_company.services .includes(:categories) .order("categories.name #{sort_order}, services.name #{sort_order}") .distinct .page(params[:page]).per(10)
-
joinsを使用
joins
を使用すると、関連するカテゴリに基づいてサービスをソートする場合に特に有効です。joins
はincludes
と異なり、Eager Loadingは行われませんが、ソートやフィルタリングには最適です。@services = current_company.services .joins(:categories) .order("categories.name #{sort_order}, services.name #{sort_order}") .distinct .page(params[:page]).per(10)
-
サブクエリの使用
より複雑なケースでは、SQLのサブクエリを使ってデータを先にフィルタリングしてからページネーションする方法が有効です。@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
テーブルが多対多の関係にあるため、同じサービスが異なるカテゴリ名で複数回リストされるのを防ぎます。
クエリの解析
ここで使用されているクエリは次のようになっています。
@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)
各部分の役割を詳しく見ていきましょう。
-
select('services.*, MIN(categories.name) as first_category_name')
- この部分は、
services
テーブルのすべてのカラムと、関連するcategories
の中で辞書順に最も早い(最小の)カテゴリ名を選択します。ここでMIN(categories.name)
を使うことで、各サービスに対して一意のカテゴリ名を確保し、その名前でソートできるようになります。
- この部分は、
-
joins(:categories)
-
services
とcategories
テーブルを内部結合します。これにより、サービスに関連するカテゴリのデータが利用できるようになります。
-
-
group('services.id')
-
services.id
でグループ化することにより、各サービスごとに集約操作(この場合はMIN
)を適用します。これがないと、MIN
関数を使用することができません。
-
-
order("first_category_name #{sort_order}, services.name #{sort_order}")
- 選択されたカテゴリ名(
first_category_name
)とサービス名(services.name
)に基づいて、結果を並べ替えます。sort_order
は動的に 'ASC' または 'DESC' を指定することができ、ソートの順序を制御します。
- 選択されたカテゴリ名(
-
page(params[:page]).per(10)
- 結果をページネーションするためのメソッドです。
page
メソッドで現在のページを設定し、per
メソッドで1ページあたりのアイテム数を指定しています。
- 結果をページネーションするためのメソッドです。
このクエリは、多対多の関係にあるデータを効率的に処理し、特定の順序でソートした結果を一貫して表示するために設計されています。さらに、ページネーションを適切に機能させるために、サービスごとにカテゴリ名を一意にする工夫がなされています。このようなアプローチは、複雑なデータモデルを扱う際に非常に役立ちます。