LoginSignup
0
0

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

Last updated at Posted at 2024-04-22

前回までの記事

はじめに

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

問題の発生

例として、ServiceCategoryが多対多の関係にあり、関連する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が異なる順番で繰り返し表示されるためです。これにより、ページあたりのサービス数が不一致になり、ページネーションがうまく機能しません。

解決策

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

    @services = current_company.services
                               .includes(:categories)
                               .order("categories.name #{sort_order}, services.name #{sort_order}")
                               .distinct
                               .page(params[:page]).per(10)
    
  2. joinsを使用
    joinsを使用すると、関連するカテゴリに基づいてサービスをソートする場合に特に有効です。joinsincludesと異なり、Eager Loadingは行われませんが、ソートやフィルタリングには最適です。

    @services = current_company.services
                               .joins(:categories)
                               .order("categories.name #{sort_order}, services.name #{sort_order}")
                               .distinct
                               .page(params[:page]).per(10)
    
  3. サブクエリの使用
    より複雑なケースでは、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)

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

  1. select('services.*, MIN(categories.name) as first_category_name')

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

    • servicescategories テーブルを内部結合します。これにより、サービスに関連するカテゴリのデータが利用できるようになります。
  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ページあたりのアイテム数を指定しています。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0