Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

【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の出番です。次のようなスコープ(クラスメソッド)を作れば、いつでも再利用できます。

lib/extensions/active_record/find_by_ordered_ids.rb
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側から生成してみましょう。

lib/extensions/active_record/find_by_ordered_ids.rb
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() 関数を使う方法(簡潔で可読性が高い)

用途や使用しているデータベースに合わせて、最適な方法を選んでみてください。

  1. Android SQLiteでレコードを降順(DESC)に表示する方法を解説

    AndroidのSQLiteとは? 実装例に入る前に、まずAndroidにおけるSQLiteデータベースについて簡単におさらいしておきましょう。SQLiteは、デバイス上のテキストファイルにデータを保存できるオープンソースのSQLデータベースです。AndroidにはSQLiteの実装が標準で組み込まれており、リレーショナルデータベースの主要な機能をすべて利用できます。さらに、JDBCやODBCのような接続確立処理を特に行う必要がないため、非常に手軽に扱える点も大きな魅力です。 本記事では、AndroidのSQLiteで登録済みのレコードを降順(DESC)で表示する方法を、サンプルコードとともに

  2. MS SQL Serverでデータベースを選択する2つの基本方法

    MS SQL Serverで操作対象となるデータベースを選択するには、主に以下の2つの方法があります。GUI操作とコマンド操作それぞれの特徴を理解し、状況に応じて使い分けることで、作業効率を高めることができます。 方法1:SQL Server Management Studio(SSMS)を使用する 視覚的に操作したい方には、SQL Server Management Studio(SSMS)を使った方法がおすすめです。 例として、msdbという名前のデータベース上でバックアップ履歴を参照するクエリを実行したい場合、ツールバーのドロップダウンリストからmsdbを選択します。 データベースのバッ