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

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

概要

原著者の許諾を得て翻訳・公開いたします。

週刊Railsウォッチ20210823 ActiveRecord::QueryMethods#in_order_ofを追加もどうぞ。

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

RailsでActive Recordを利用していて、クエリの結果が特定の順序に並んでいることを期待する場合があります。

たとえば、ブックリーダー用のRailsアプリケーションがあり、読了した本や今呼んでいる本やこれから読みたい本をトラッキングできるとします。

このアプリケーションをシンプルに構築するには、UserBookを作成します。このモデルにはbook_iduser_idstatusという3つのカラムがあります。statusカラムはreadcurrently_readingto_readのいずれかの値を取れるようになっています。

変更前

ユーザーの本をto_readcurrently_readingreadの順で表示するには、以下のような実装が考えられます。

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_ofActiveRecord::Relationオブジェクトに対して動作する点が異なります。

編集部注(2022/10/14)

後に以下のプルリクが7-0-stableブランチにマージされ、Enumerable#in_order_ofの挙動に合わせてActiveRecord::QueryMethods#in_order_ofが値のリストに合わせてレコードをWHEREで絞り込むように変更されました。

関連記事

Rails 7: insert_allとupsert_allで属性のエイリアスを指定可能になる(翻訳)


CONTACT

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