ActiveRecordを使っていてunionしたいパターンができたので、やってみようと思ったら、思いの外ハマってしまったので情報共有したいと思います。
ActiveRecordにunionというメソッドがあることを知った私は、これでできるんじゃないの?と思って意気揚々と使ったのですが、なぜかエラーがおきました。
union = MyGroup.where(user_id: user).reorder(nil).union(
MyGroup.limited.where(user_id: user.members).reorder(nil)
)
MyGroup.from(MyGroup.arel_table.create_table_alias(union, :my_groups).to_sql)
MyGroup.limited
は、公開範囲をActiveRecord::Enumを使っていて、限定公開をlimitedというスコープで作っています。
これを実行すると…エラーになります。
PG::ProtocolViolation: ERROR: bind message supplies 0 parameters, but prepared statement "" requires 1
: SELECT "my_groups".* FROM ( SELECT "my_groups".* FROM "my_groups" WHERE "my_groups"."user_id" = 209 UNION SELECT "my_groups".* FROM "my_groups" WHERE "my_groups"."status" = $1 AND "my_groups"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."id" IN (255, 256, 209)) my_groups
"my_groups"."status" = $1
になっていて、limitedに割り当てられた数値になっていません。
unionメソッドがうまく使えなかった原因
原因は上記の通り、to_sqlを使ったタイミングでActiveRecordの検索条件に設定した値が置き換えられていなかったからです。to_sqlメソッドでSQL文を受け取る時には置換されていると思っていたのですが、unionメソッドを使うと戻り値がActiveRecord::Relation
ではなく、Arel::Nodes::Union
になるようです。そして、ActiveRecord::Relation
のto_sqlメソッドとArel
のto_sqlメソッドでは動作が違う模様です。
参考URL:to_sql in Rails 4.2 returns parameterized queries instead of full SQL statements
参考URLでリンクされているチケットのどれかに書いてありましたが、『機能の不足はバグではない』、という言葉で終わっていて、とりあえずバグとして対応されるということはなさそうです。
対応方法
ダメだったやつ(unprepared_statement)
参考URLでは、unprepared_statement
を使えばいいと書いてありましたが、これを使ってもダメでした。
union_sql = MyGroup.connection.unprepared_statement do
MyGroup.where(user_id: user).reorder(nil).union(
MyGroup.limited.where(user_id: user.members).reorder(nil)
).to_sql
end
MyGroup.from("#{union_sql} my_groups")
エラーの内容は変わらず。
うまくいったやつ(arel_tableを使う)
Arelのto_sqlの結果が云々とあったので、該当箇所のwhere文をarel_tableを使ったものに変更すればうまくいくのでは?と思い、やってみたところ、うまくいきました。
union = MyGroup.where(user_id: user).reorder(nil).union(
# limitedスコープをarel_tableを使って表現するよう変更
MyGroup.where(user_id: user.members).
where(MyGroup.arel_table[:status].eq MyGroup.statuses[:limited]).reorder(nil)
)
MyGroup.from(MyGroup.arel_table.create_table_alias(union, :my_groups).to_sql)
まとめ
ArelにはArelをぶつけよう。