LoginSignup
0
1

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

Posted at

はじめに

実装したこと

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

このスコープの目的は、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として別名を付けています。これにより、お気に入りの数がカウントされた仮想的なカラムを取得できます。

さらに詳しく

ここでは、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モデルをお気に入りの多い順にソートする際に使用されます。

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

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

スコープとは

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

スコープは、クエリの一部を再利用可能なブロックとして定義します。-> { ... }の部分がブロックを表します。このスコープを使うと、例えば以下のようにコントローラーで呼び出すことができます:

class ContentsController < ApplicationController
  def index
    @contents = Content.ordered_by_favorite_count
  end
end

このように、スコープを使用すると、複雑なクエリを簡潔に書くことができ、モデルの再利用性と保守性を高めることができます。

左外部結合とは

「テーブルと左外部結合する」とは、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が入ります。このようにして、関連するデータを結合することができます。

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>

だいぶ省略はしておりますが、上記のようなリンクで作動するようになります。

参考にさせていただいたサイト

0
1
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
1