[Rails]仮想カラムをもちいて、ソート機能を実装する

## はじめに
#### 実装したこと
PFでコミュニティサイトを作成しており、投稿一覧にソート機能を実装しようとしております。「いいねが多い順」のソート機能の実装が思ったより難しかったのでアウトプットのため、記事に残します。

#### 動作環境
Ruby 3.1.2
Rails 6.1.7.3

## 手順
#### モデルとカラムの状態
~~~shema.rb
  create_table "contents", force: :cascade do |t|
    t.string "title", null: false
    t.text "text"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "favorites", force: :cascade do |t|
    t.integer "content_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
~~~

contentモデルとfavoriteモデルが存在しており、contentモデルにfavorite_countを仮想カラムに設定する流れを以下に記載していきます。

#### model
~~~content.rb
class Content < ApplicationRecord
    has_many :favorites, dependent: :destroy
    
    # お気に入りの多い順にソートする'スコープ'を定義します
    scope :ordered_by_favorite_count, -> {
      left_joins(:favorites)
        .select('contents.*, COUNT(favorites.id) AS favorite_count')
        .group('contents.id')
        .order('favorite_count DESC')
    }
end
~~~
:::note info
このスコープの目的は、contentモデルのレコードをお気に入りの数（favorite_count）の多い順にソートすることです。しかし、実際には物理的なデータベーステーブルにfavorite_countカラムを追加せずに、SQLクエリを使用して仮想的にソートする方法を取ります。
:::

##### ステップごとに今回のスコープを説明します。

1\. **left_joins(:favorites)**: Contentモデルをfavoritesテーブルと左外部結合します。これにより、お気に入り(favorites)がないContentレコードも結果に含まれるようになります。

2\. **.select('contents.*, COUNT(favorites.id) AS favorite_count')**: contentsテーブルの全てのカラムを取得しますが、同時にfavoritesテーブルのidカラムをカウントして、favorite_countとして別名を付けています。これにより、お気に入りの数がカウントされた仮想的なカラムを取得できます。
<details><summary>さらに詳しく</summary>

ここでは、contentsテーブルに対してfavoritesテーブルとの結合を行い、それによってfavoritesテーブルの情報を取得します。しかし、結合した結果を元のcontentsテーブルのカラムと合わせて1つのレコードにしたいため、contents.*として、contentsテーブルの全てのカラムを選択します。

次に、favoritesテーブルのidカラムをカウントするためにCOUNT(favorites.id)を使います。これはfavoritesテーブルの行数をカウントする操作です。favoritesテーブルの各行に対して、idカラムが存在する（NULLではない）場合にカウントされます。

そして、COUNT(favorites.id)の結果を仮想的なカラムとして、AS favorite_countでfavorite_countという名前のカラムとして選択します。これにより、favorite_countというカラムが結果に含まれることになります。このカラムには、各contentsレコードに対してfavoritesテーブルの関連するレコード数（お気に入りの数）が入ります。

結果として得られるクエリ結果は、contentsテーブルの全てのカラムが表示され、さらに仮想的なfavorite_countカラムが加わっています。この仮想的なカラムには、お気に入りの数（favoritesテーブルのidカウント）が格納されることになります。これにより、Contentモデルをお気に入りの多い順にソートする際に使用されます。
</details>

3\. **.group('contents.id')**: contentsテーブルのidカラムでグループ化します。これにより、結果がcontentsテーブルのレコードごとに1行になります。

4\. **.order('favorite_count DESC')**: favorite_countを降順（多い順）でソートします。つまり、お気に入りの多い順に並べ替えられます。
結果として得られるクエリは、お気に入り数（favorite_count）が最も多いContentレコードから順にソートされたクエリとなります。データベースの実際のカラムを変更することなく、仮想的にお気に入りの多い順にソートすることができます。

<details><summary>スコープとは</summary>

スコープは、Active Recordモデルに定義されるクエリの再利用を可能にする方法です。特定のクエリを頻繁に使用する場合、スコープを使ってそのクエリを定義し、モデルの他の箇所から再利用できるようにします。先ほどのコードで示した'ordered_by_favorite_count'スコープは、contentモデルに定義されたクエリの再利用のためのものです。このスコープにより、contentモデルをお気に入りの数（favorite_count）の多い順にソートするためのクエリが定義され、他の箇所で再利用できるようになります。

スコープは、クエリの一部を再利用可能なブロックとして定義します。-> { ... }の部分がブロックを表します。このスコープを使うと、例えば以下のようにコントローラーで呼び出すことができます：
~~~ruby
class ContentsController < ApplicationController
  def index
    @contents = Content.ordered_by_favorite_count
  end
end
~~~
このように、スコープを使用すると、複雑なクエリを簡潔に書くことができ、モデルの再利用性と保守性を高めることができます。
</details>

<details><summary>左外部結合とは</summary>

「テーブルと左外部結合する」とは、2つのテーブルを結合する方法の一つです。結合は、関連するデータを取得するためのデータベース操作です。左外部結合は、結合する2つのテーブルのうち、左側のテーブルの全ての行を含み、右側のテーブルとの共通のキーに基づいてマッチングする行を取得します。

データベースでは、主に2つのテーブルを結合する際に使用されます：

内部結合（Inner Join）: 共通のキーに基づいて2つのテーブルからデータを結合します。共通のキーがない行は結果に含まれません。

外部結合（Outer Join）: 左外部結合（Left Outer Join）と右外部結合（Right Outer Join）の2つがあります。左外部結合は、左側のテーブルの全ての行を含み、右側のテーブルとの共通のキーに基づいてマッチングする行を取得します。右外部結合はその逆で、右側のテーブルの全ての行を含み、左側のテーブルとの共通のキーに基づいてマッチングする行を取得します。

例えば、以下のようなcontentsテーブルとfavoritesテーブルがあるとします。

contentsテーブル:
~~~
id | title
---------
1  | Content A
2  | Content B
3  | Content C
~~~

favoritesテーブル:
~~~
id | content_id | user_id
-------------------------
1  | 1          | 101
2  | 1          | 102
3  | 2          | 103
~~~
左外部結合では、contentsテーブルを左側、favoritesテーブルを右側として結合します。そして、contentsテーブルの全ての行を含み、content_idを共通のキーとしてfavoritesテーブルとマッチングします。結果は以下のようになります。

~~~
contents.id | contents.title | favorites.id | favorites.content_id | favorites.user_id
------------------------------------------------------------------------------
1           | Content A      | 1            | 1                    | 101
1           | Content A      | 2            | 1                    | 102
2           | Content B      | 3            | 2                    | 103
3           | Content C      | NULL         | NULL                 | NULL
~~~
左外部結合により、contentsテーブルの全ての行が残り、対応するfavoritesテーブルのデータがあれば含まれ、ない場合はNULLが入ります。このようにして、関連するデータを結合することができます。
</details>

#### controller
~~~contents.controller.rb
class User::ContentsController < ApplicationController

  def index
    if params[:sort] == 'favorite_desc'
      @contents = Content.page(params[:page]).ordered_by_favorite_count
    else 
      @contents = Content.page(params[:page]).order(created_at: :asc)
    end
  end

end
~~~
これで、@contents変数にはお気に入りの多い順にソートされたContentレコードが格納されます。

#### views
~~~index.html
<div class="card-body">
  <%= link_to "いいねが多い順", contents_path(sort: 'favorite_desc') %>
</div>
~~~
だいぶ省略はしておりますが、上記のようなリンクで作動するようになります。

## 参考にさせていただいたサイト
https://pikawaka.com/rails/scope

https://wa3.i-3-i.info/word15311.html