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