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
、続いてsubmitted
やhold
、という具合に結果が返されます。
しかし別のソート順で結果を得たい場合はどうすればよいでしょうか。そんなとき、つい以下のように書いてしまいたくなるかもしれません。
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 APIドキュメント:
ActiveRecord::QueryMethods#in_order_of
関連記事
Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of
-
編注:
QueryMethods#in_order_of
は、先行して追加されたEnumerable#in_order_of
の挙動に合わせるために#44097でリストにないレコードを返さないようになりました(参考: 週刊Railsウォッチ20220117)。 ↩
概要
元サイトの許諾を得て翻訳・公開いたします。
参考: 週刊Rails週刊Railsウォッチ20210831
ActiveRecord::QueryMethods#in_order_of
を追加