Railsでraw SQLをActiveRecord::RelationにしてPagyに渡す

RailsでUNIONを使って生SQLを書く必要があった。
その結果をPagyに渡すとArrayではなくActiveRecord::Relationで渡せと怒られた。

find_by_sqlはArrayを返すし、select_allはActiveRecord::Resultを返すので困った。
どうしても生SQLの結果をActiveRecord::Relationにしたかったのでメモ。

Array でも渡せることに途中で気付いたが、データを全部メモリに乗せることになるため可能なら避けたい)

user.rb

##
# user.status = 1 のユーザが先頭表示されるようにしつつ、
# それ以降は created_at の降順にソートするUNION
#
# @param [UserGroup] group
# @return [String]
def self.users_query(group)
  unless project.instance_of?(UserGroup)
    raise "invalid parameter."
  end

  # bigint (unsigned) の最大値
  limit = 18446744073709551615

  query1 = User
    .where(user_group: group)
    .where(status: 1)
    .order(created_at: :desc)
    .limit(limit)
    .to_sql

  query2 = User
    .where(user_group: group)
    .where.not(status: 1)
    .order(created_at: :desc)
    .limit(limit)
    .to_sql

  "(#{query1}) UNION ALL (#{query2})".chomp
end

UNIONでそれぞれのSELECTごとにソートしたい場合はLIMITの指定が必要。
https://dev.mysql.com/doc/refman/5.7/en/union.html#union-order-by-limit

上記の例では status の where 句により query1 / query2 のデータが重複することはあり得ないので、UNION よりも高速な UNION ALL を使っている。重複する可能性があるなら UNION を使う。

users_controller.rb

def sample
  group = UserGroup.find(1)

  query = User.users_query(group)
  users = User.from("(#{query}) as users")

  @pagy, @users = pagy(
    users,
    page: params[:page],
    items: 10,
  )

  render json: { data: @users, meta: pagy_metadata(@pagy) }
rescue Pagy::OverflowError => e
  render json: { data: [], meta: { count: 0, page: 1, items: 0, pages: 1 } }
end

from の結果は ActiveRecord::Relation になるので、そのままPagyに渡せる。
実際のクエリは以下のようになる。

SELECT `users`.* FROM (
	(
		SELECT `users`.* FROM `users` 
		WHERE `users`.`group_id` = 1 
		AND `users`.`status` = 1 
		ORDER BY `users`.`created_at` DESC 
		LIMIT 18446744073709551615
	)
	UNION ALL
	(
		SELECT `users`.* FROM `users` 
		WHERE `users`.`group_id` = 1 
		AND `users`.`status` != 1 
		ORDER BY `users`.`created_at` DESC
		LIMIT 18446744073709551615
	)
) as users

参考記事