いろんなことの合せ技のためご利用は計画的に、という感じであるが知見まで。
環境
$ bin/rails -v Rails 6.0.3.1
今回の想定ケース
例えばUserクラスがあり、Viewにはidが2,3,1の順で並べたいのでActiveRecordの取得自体も2,3,1の順番で取得したい場合。
User.where(id:[2,3,1])
上記のような書き方が想定できるが、その場合はただただ IN
句で絞り込むだけなので並び順は考慮されない ActiveRecord_Relation
が返却されてしまう。
User.where(id:[2,3,1]).each do |u| pp u.id end # User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (2, 3, 1) # 1 # 2 # 3
対応方法
MySQLのみとはなるが、以下のようなクエリを書くことで実現することができる。
User.order([Arel.sql('field(id, ?)'), [3,1,2]])
ids = [2,3,1] User.where(id:ids).order([Arel.sql('field(id, ?)'), ids]).each do |u| pp u.id end # User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (2, 3, 1) ORDER BY field(id, 2,3,1) # 2 # 3 # 1
なぜ ORDER BY field(カラム名,2,3,1)
のようなクエリで任意の並びになるのか
詳しい解説は下記ページに記載がある
日々の覚書: WHERE .. IN (..)のリストの順番でソートするORDER BY FIELDの仕組み
引用させていただくと以下
mysql56> SELECT * FROM t1 WHERE num in (7, 5, 3) ORDER BY FIELD(num, 7, 5, 3);
初めて見た時はファッ!? ってなったけど、クエリーをこう書き換えると、たぶんやってることが伝わる。
mysql56> SELECT *, FIELD(num, 7, 5, 3) AS sort_rank FROM t1 WHERE num in (7, 5, 3) ORDER BY sort_rank;
ORDER BY FIELDはORDER BY句のバリエーションじゃなくて、FILED関数の結果でORDER BYしている。 FIELD関数のドキュメントはこちら。第1引数に検索したい値、第2引数以降に検索元となるリストを与える感じ。 これが、numの値が(7, 5, 3)の何番目にあるかを整数で返すので、そこでソートできる。
とのことなので
- FIELD関数を使って任意の順番とソート順番のマッピングリストを生成する
- 上記リストを利用してorderで並べ替えを行う
という原理。
なぜ Arel.sql
で囲むのか
これはこの実装がされたPRを見るところ
User.order(params["order_string"])
のような無邪気な実装でSQLインジェクションが実行されないように基本的には遮断するという実装となった。
その際にもし指定するSQL安全なものであれば Arel.sql
で囲むことで回避ができる、という考えで Arel.sql
で囲むことでの回避手段が提供された。
そのため、FIELDを使う場合はちゃんと利用する値にSQLインジェクションの危険性のないものかをしっかり確認してから実装したほうがよい。