Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ransackでソートを実装したい人のための最短ルート

はじめに

2025年4月に入社して、気づけばもう12月です。環境の変化もあり1年あっという間でした。
前回の記事では新卒でフルリモート環境がどうかって話をしましたが、今はこの環境に慣れてしまってしっかりと仕事ができています。
今回の記事では8月から新規プロジェクトに参加してRansackを使用してソート機能を実装するにあたり、自分がつまずいたポイントを踏まえて、これから実装する人が最短ルートで進められるようにまとめました。
※本記事ではRansack 4.3.0を使用しています。

この記事でやること

  • Ransackとは
  • Ransackの導入
  • ソート機能の実装
    • sort_linkを使用した一般的なソート機能の実装
    • def self.ransackable_attributesとは
    • def self.ransackable_associationsとは
  • デフォルトでソートされている属性について
  • ソートアイコンの変更について

Ransackとは?

Railsアプリケーションに検索機能やソート機能を実装することができます。

activerecord-hackery/ransack - GitHub

Ransackの導入

Gemfileに下記のコードを追加してbundle installを実行すると導入完了です。

gem 'ransack'

ソート機能の実装

下記のような表があったとします。
この場合、並び順はアカウントが作成された順で表示されています。

@users = User.order(created_at: :desc)

ユーザ一覧が表示されている画像

名前順で並び替えをしたければ、~.order(name: :asc)とすれば良いでしょうか。

ソートされたユーザ一覧画像

名前順で並び替えができました。
しかしこれでは並び替えをしたければコードを変更するしかありません。
そのため、一般的なソート機能の動きを実装するため下記のようなソート機能を実装しようと思います。

一般的なソートの説明画像

sort_linkを使用した一般的なソート機能の実装

sort_linkをviewに記述することでクリックするとクリックされた列でソートすることができます。
sort_link(Ransackのオブジェクト, ソート対象のカラム名, 表示するリンクテキスト)のように扱います。

before

<div class="users">
  <h1>Users</h1>
  <table class="users-table">
    <thead>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Email</th>
        <th>Created</th>
      </tr>
    </thead>
    <tbody>
      <% @users.each do |user| %>
        <tr>
          <td><%= user.id %></td>
          <td><%= user.name %></td>
          <td><%= user.email %></td>
          <td><%= l(user.created_at, format: :short) %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>

after

 <div class="users">
  <h1>Users</h1>
  <table class="users-table">
    <thead>
      <tr>
        <th><%= sort_link(@q, :id, "ID") %></th>
        <th><%= sort_link(@q, :name, "Name") %></th>
        <th><%= sort_link(@q, :email, "Email") %></th>
        <th><%= sort_link(@q, :created_at, "Created") %></th>
      </tr>
    </thead>
    <tbody>
      <% @users.each do |user| %>
        <tr>
          <td><%= user.id %></td>
          <td><%= user.name %></td>
          <td><%= user.email %></td>
          <td><%= l(user.created_at, format: :short) %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>

しかしこれではエラーが発生します。

エラー画面

controllerでRansackのオブジェクトを宣言する必要があります。

def index
  @q = User.ransack(params[:q])
  @users = @q.result.order(created_at: :desc)
end

これでsort_linkを使用して一般的なソート機能を実装することができました。

ユーザ一覧画面

def self.ransackable_attributesとは

実は先ほどの実装ではページの表示はできますが、リンクをクリックしてソートしようとするとエラーが発生します。

エラー画面

※ここでは検索という言葉で説明していますが、ソートする場合も同様です

要は「Ransackは検索可能として明示的に許可された属性が必要」ということで、モデルの全カラムを検索ができてしまう状態だとセキュリティ的に危険なので、どの属性を検索して良いか指定しないとエラーになってしまいます。
エラー文から分かるように、Userモデルにはid, name, email, created_at以外にも様々な属性が存在しています。そのため、意図しない操作を防ぐためにどの属性を検索可能にするか指定する必要があります。
今回はid, name, email,created_atのみでいいのでmodels/user.rbに下記のコードを追加します。

class User < ApplicationRecord
  def self.ransackable_attributes(auth_object = nil)
    %w[id name email created_at]
  end
end

これで一般的なソート機能が実装できました。

ユーザ一覧画面

ユーザ一覧画面

def self.ransackable_associationsとは

もし、このようにincludesされていた場合どうなるでしょうか。

def index
  @q = Post.includes(:user).ransack(params[:q])
  @posts = @q.result.order(created_at: :desc)
end

ポスト一覧画面の説明
先ほどのransackable_attributesの設定はこのようになります。

class Post < ApplicationRecord
  def self.ransackable_attributes(auth_object = nil)
    %w[id content created_at]
  end
end

これで、ID, Content, Createdのソート機能は実装できました。
しかし、Userはどうでしょうか。先ほどのransackable_attributesにはnameを追加していません。
もちろん追加してもPostモデルにはnameという属性は存在しないため意味はありません。
関連がある場合Ransackのソートはややこしくなります。
もしこの状態でUserのソートをしようとするとこのようにエラーが発生します。

エラー画面

Postモデルでassociationを検索条件として使いたいなら、どのassociationを検索に使っていいのかRansackに明示する必要があるってことです。
今回の場合だと、Userモデルを検索条件として使いたいので、ransackable_associationsにUserを指定する必要があります。

class Post < ApplicationRecord
  def self.ransackable_associations(auth_object = nil)
    %w[user]
  end
end

デフォルトでソートされている属性について

Ransackではデフォルトでソートされる属性を指定することができます。
.order()と似ていますが、Ransackで用意されている方法を使うと何がいいのか、それはページにアクセスしたときソートされている属性に▲▼を表示することができます。
アクセスした時に▲▼が表示されていることで、ユーザに初期状態ではどの項目がソートされているのかを知らせることができます。.orderだとこれができません。

def index
  @q = User.ransack(params[:q])
  @q.sorts = 'created_at desc' if @q.sorts.empty?
  @users = @q.result
end

ユーザ一覧画面

以上で「Ransackでソート機能を実装して学んだこと」について終わります。
2025年もあと少しです。来年も健康に気をつけて全力で人生を楽しみつつ仕事もこなしていこうと思いいます。ではではノシ


BPSアドベントカレンダー2025


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。