Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of
RailsでActive Recordを利用していて、クエリの結果が特定の順序に並んでいることを期待する場合があります。
たとえば、ブックリーダー用のRailsアプリケーションがあり、読了した本や今呼んでいる本やこれから読みたい本をトラッキングできるとします。
このアプリケーションをシンプルに構築するには、UserBook
を作成します。このモデルにはbook_id
とuser_id
とstatus
という3つのカラムがあります。status
カラムはread
、currently_reading
、to_read
のいずれかの値を取れるようになっています。
変更前
ユーザーの本をto_read
、currently_reading
、read
の順で表示するには、以下のような実装が考えられます。
user = User.first
# Arelを利用
result = user.user_books.
order(
Arel.sql(
%q(
case status
when 'to_read' then 1
when 'currently_reading' then 2
when 'read' then 3
end
)
)
)
# クエリをかけてからレコードを並べ替える
result = user.user_books.where(status: %w[to_read currently_reading read])
# 以下のアプローチか
# 「オランダ国旗問題」のソリューションを使える
# https://en.wikipedia.org/wiki/Dutch_national_flag_problem
ordered_result = result.collect{ |user_book| user_book.status == "to_read" } +
result.collect{ |user_book| user_book.status == "currently_reading" } +
result.collect{ |user_book| user_book.status == "read" }
指定の順序で最終的な結果をマッピングするには、ArelでSQL文を書くか、クエリレコードを反復しなければなりません。
変更後
Rails 7のActiveRecord::QueryMethods
に、上述の問題を解決するin_order_of
メソッドが追加されました(#42061)。
この新しい変更を用いれば、上述の実装は以下のようになります。
user = User.first
result = user.user_books.in_order_of(:status, %w[to_read currently_reading read])
#=> #<ActiveRecord::Relation [#<UserBook id: 3, user_id: 1, status: "to_read">, #<UserBook id: 4, user_id: 1, status: "to_read">, #<UserBook id: 5, user_id: 1, status: "currently_reading">, #<UserBook id: 6, user_id: 1, status: "read">]>
UserBook.in_order_of
は以下のクエリを生成します。
SELECT "user_books".* FROM "user_books" /* loading for inspect */ ORDER BY CASE "user_books"."status" WHEN 'to_read' THEN 1 WHEN 'currently_reading' THEN 2 WHEN 'read' THEN 3 ELSE 4 END ASC LIMIT ? [["LIMIT", 11]]
ただしMySQLの場合、CASE
の代わりにFIELD
関数が用いられます。
SELECT "user_books".* FROM "user_books" /* loading for inspect */ ORDER BY FIELD("user_books"."status", 'to_read', 'currently_reading', 'read') ASC
Rails 7には既に、ActiveRecordのin_order_of
と同様に振る舞うEnumerable#in_order_of
も追加されています(関連記事)。
Enumerable#in_order_of
はEnumeratorに対して動作しますが、ActiveRecordのin_order_of
はActiveRecord::Relation
オブジェクトに対して動作する点が異なります。
編集部注(2022/10/14)
後に以下のプルリクが7-0-stableブランチにマージされ、Enumerable#in_order_of
の挙動に合わせてActiveRecord::QueryMethods#in_order_of
が値のリストに合わせてレコードをWHEREで絞り込むように変更されました。
概要
原著者の許諾を得て翻訳・公開いたします。
週刊Railsウォッチ20210823 ActiveRecord::QueryMethods#in_order_ofを追加もどうぞ。