コード日進月歩

しんくうの技術的な小話、メモ、つれづれ、など

Rails6かつMySQLを使う場合に任意の順番でレコードを取得する

いろんなことの合せ技のためご利用は計画的に、という感じであるが知見まで。

環境

$ 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インジェクションの危険性のないものかをしっかり確認してから実装したほうがよい。

参考リンク