【Rails】IDを指定してレコードを任意の順序で取得する方法(CASE式・FIELD関数)
Railsでは、IDがわかっていればデータベースから複数のレコードを簡単に取得できます。
Person.where(id: [1, 2, 3]).map(&:id) # => [1, 2, 3]
しかし、取得したレコードを別の順序で受け取りたい場合はどうすればよいのでしょうか?たとえば、検索エンジンが関連度の高い順にIDを返してくるようなケースです。その順序を保ったままレコードを取得するには、どうすればいいのでしょうか。
同じく where を使って次のように書いても、うまくいきません。
Person.where(id: [2, 1, 3]).map(&:id) # => [1, 2, 3]
IDを渡した順序は無視され、結果は必ず主キーの昇順になってしまいます。では、どうすれば指定した通りの順序でレコードを取得できるのでしょうか。この記事では、その方法を2つ紹介します。
互換性重視の方法:SQLのCASE式を使う
Rubyと同じように、SQLにも CASE...WHEN 式が用意されています。
CASE...WHEN はあまり目にする機会が多くありませんが、ある値を別の値へマッピングするという使い方ができます。Rubyで書くとイメージしやすいでしょう。
case :b
when :a then 1
when :b then 2
when :c then 3
end # => 2
このコードは、実質的に次のようなハッシュと同じ動作をしています。
{
:a => 1,
:b => 2,
:c => 3
}[:b] # => 2
つまり、「キー → 表示順」へのマッピングが作れるということです。これを利用すれば、データベース側でその任意の順序どおりにソートして結果を返してもらえます。
そこで、各IDとその表示位置を CASE 式に組み込み、SQLの ORDER BY 句に渡すというのが基本的なアイデアです。
たとえば、レコードを [2, 1, 3] の順序で取得したい場合、SQLは次のようになります。
SELECT * FROM people
WHERE id IN (1, 2, 3)
ORDER BY CASE id
WHEN 2 THEN 0
WHEN 1 THEN 1
WHEN 3 THEN 2
ELSE 3 END;
これで、レコードは意図したとおりの順序で返されます。CASE 式が、それぞれのIDを「何番目に返すべきか」という数値に変換しているわけです。
もちろん、これを手作業で書くのは非現実的ですよね。でも心配いりません。そういうときこそRubyの出番です。次のようなスコープ(クラスメソッド)を作れば、いつでも再利用できます。
module Extensions::ActiveRecord::FindByOrderedIds
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index])
end
order_clause << sanitize_sql_array(["ELSE ? END", ids.length])
where(id: ids).order(order_clause)
end
end
end
ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds)
Person.find_ordered([2, 1, 3]) # => [2, 1, 3]
期待どおり、[2, 1, 3] の順序でレコードが返ってきました。ポイントは sanitize_sql_array を使ってプレースホルダ経由で値を埋め込んでいる点です。これにより、SQLインジェクションのリスクを避けながら安全にORDER BY句を組み立てられます。
MySQL限定ならもっとシンプル:FIELD関数を使う
MySQLを使っているなら、さらに読みやすく書きやすい方法があります。MySQLには ORDER BY FIELD という専用構文が用意されているのです。
SELECT * FROM people
WHERE id IN (1, 2, 3)
ORDER BY FIELD(id, 2, 1, 3);
FIELD() 関数は、第1引数の値が第2引数以降のリストの中で何番目にあるかを返します。つまり、CASE式を自前で組み立てなくても、IDのリストを渡すだけで同じことが実現できます。
こちらもRuby側から生成してみましょう。
module Extensions::ActiveRecord::FindByOrderedIds
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
sanitized_id_string = ids.map {|id| connection.quote(id)}.join(",")
where(id: ids).order("FIELD(id, #{sanitized_id_string})")
end
end
end
ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds)
MySQLを使っていて、他のデータベースへの移植性をそれほど気にしないのであれば、この方法がおすすめです。ログに出力されるSQL文も格段に読みやすくなります。
まとめ:並べ替えはデータベースに任せよう
レコードを特定の任意の順序で表示したいとき、わざわざRuby側でソートする必要はありません。今回紹介した小さなコードスニペットを使えば、検索・並べ替え・データの返却といった得意分野をデータベースに任せることができます。
- 汎用的に使いたい場合: SQL標準の
CASE式を使う方法(PostgreSQLやSQLiteなどでも動作) - MySQL限定でよい場合:
FIELD()関数を使う方法(簡潔で可読性が高い)
用途や使用しているデータベースに合わせて、最適な方法を選んでみてください。
-
Android SQLiteでレコードを降順(DESC)に表示する方法を解説
AndroidのSQLiteとは? 実装例に入る前に、まずAndroidにおけるSQLiteデータベースについて簡単におさらいしておきましょう。SQLiteは、デバイス上のテキストファイルにデータを保存できるオープンソースのSQLデータベースです。AndroidにはSQLiteの実装が標準で組み込まれており、リレーショナルデータベースの主要な機能をすべて利用できます。さらに、JDBCやODBCのような接続確立処理を特に行う必要がないため、非常に手軽に扱える点も大きな魅力です。 本記事では、AndroidのSQLiteで登録済みのレコードを降順(DESC)で表示する方法を、サンプルコードとともに
-
MS SQL Serverでデータベースを選択する2つの基本方法
MS SQL Serverで操作対象となるデータベースを選択するには、主に以下の2つの方法があります。GUI操作とコマンド操作それぞれの特徴を理解し、状況に応じて使い分けることで、作業効率を高めることができます。 方法1:SQL Server Management Studio(SSMS)を使用する 視覚的に操作したい方には、SQL Server Management Studio(SSMS)を使った方法がおすすめです。 例として、msdbという名前のデータベース上でバックアップ履歴を参照するクエリを実行したい場合、ツールバーのドロップダウンリストからmsdbを選択します。 データベースのバッ