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

Railsの技: Active Recordの結果のenum値ソートをSQLで実行する(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

参考: 週刊Rails週刊Railsウォッチ20210831ActiveRecord::QueryMethods#in_order_ofを追加

Railsの技: Active Recordの結果のenum値ソートをSQLで実行する(翻訳)

Railsのenumは、Active Recordでステータスなどをモデリングするのに適した方法です。enumは、人間にとって読みやすいメソッドのセットを提供すると同時に、結果をデータベースに整数値で保存します。

class JobSubmission < ApplicationRecord
  enum status: {
    draft: 0,
    submitted: 1,
    hold: 2,
    rejected: 3,
    accepted: 4,
    canceled: 5
  }
end

enum値を明示的に定義するときはHashを使うことを強くおすすめします。そうしないと、Railsがenum値をデータベースに保存するときにenum値のインデックスを使うので、enumオプションの順序を変えたり削除したりすると、参照のマッピングが壊れてしまいます。

他のカラムと同様に、enum値でソートするとその値が使われます。

JobSubmission.all.order(:status)
# "SELECT \"job_submissions\".* FROM \"job_submissions\" ORDER BY \"job_submissions\".\"status\" ASC

上はJobSubmissionの結果を昇順で返すので、最初にdraft、続いてsubmittedhold、という具合に結果が返されます。

しかし別のソート順で結果を得たい場合はどうすればよいでしょうか。そんなとき、つい以下のように書いてしまいたくなるかもしれません。

class JobSubmission < ApplicationRecord
  STATUS_SORT = {
    accepted: 0,
    hold: 1,
    submitted: 2,
    draft: 3,
    rejected: 4,
    canceled: 5
  }

  def status_sort_value
    STATUS_SORT[status]
  end
end

JobSubmission.all.sort(&:status_sort_value)

しかし、このソートはデータベースではなくRubyで行われるので、結果セットが大きい場合はパフォーマンスが低下する可能性があります。

そうする代わりに、in_order_ofでenum値のソート順を指定する方法が使えます。この方法の最も優秀な点は、ソートがSQL内で行われることです(背後でCASEコマンドが使われます)。

使い方

JobSubmissionのレコードのソートを、status enum値の順序を変えて行うには、以下のように書きます。

JobSubmission.all.in_order_of(:status, %w[accepted hold submitted draft rejected canceled])

第1引数はソートするカラム名、第2引数はソートしたい順序に並べた値の配列です。

このクエリで生成されるSQLは以下のようになります。

SELECT "job_submissions".*
FROM "job_submissions"
  WHERE "job_submissions"."status" IN (4, 2, 1, 0, 3, 5)
ORDER BY
  CASE "job_submissions"."status"
    WHEN 4 THEN 1
    WHEN 2 THEN 2
    WHEN 1 THEN 3
    WHEN 0 THEN 4
    WHEN 3 THEN 5
    ELSE 6
  END ASC

このリレーションに別のソートを追加することも可能です。たとえば、レコードをステータスでソートした後、nameカラムでアルファベット順ソートする場合が考えられます。

JobSubmission.all
  .in_order_of(:status, %w[accepted hold submitted draft rejected canceled])
  .order(:name)

付記

重要な注意点は、in_order_ofがenum値に対してWHERE/IN句も追加することです。つまり、in_order_ofを呼び出すときに指定したenum値を持つレコードだけが返され、指定したenum値を持たないレコードは返されません1。これはある意味驚きの挙動であり、enum値を頻繁に変更する場合はこの点に注意が必要です。

この順序指定をさまざまな場所で繰り返し書くことを避けるために、おそらく以下のようにモデル内でソート順をスコープ化するとよいでしょう。

class JobSubmission < ApplicationRecord
  scope :in_status_order, -> { in_order_of(:status, %w[accepted hold submitted draft rejected canceled]) }
end

JobSubmission.in_status_order.order(:name)

参照資料

関連記事

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of


  1. 編注: QueryMethods#in_order_ofは、先行して追加されたEnumerable#in_order_ofの挙動に合わせるために#44097でリストにないレコードを返さないようになりました(参考: 週刊Railsウォッチ20220117)。 

CONTACT

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