状況
(Rails 5.0.0 にて確認)
class A
belongs_to :B
end
class B
has_one :A
has_one :C
scope :with_c, -> { joins(:c) }
scope :without_c, -> { left_outer_joins(:c).merge(C.where(id: nil)) }
end
class C
belong_to :B
end
> B.with_c.to_sql
SELECT "bs".* FROM "bs" INNER JOIN "cs" ON "cs"."b_id" = "bs"."id"
> A.joins(:b).merge(B.with_c).to_sql
SELECT "as".* FROM "as" INNER JOIN "bs" ON "bs"."id" = "as"."b_id" LEFT OUTER JOIN "cs" ON "cs"."b_id" = "bs"."id"
> B.without_c.to_sql
SELECT "bs".* FROM "bs" INNER JOIN "cs" ON "cs"."b_id" = "bs"."id" WHERE "cs"."id" IS NULL
> A.joins(:b).merge(B.without_c).to_sql
ActiveRecord::ConfigurationError: Can't join 'A' to association named 'c'; perhaps you misspelled it?
from /path/to/activerecord-5.0.0/lib/active_record/associations/join_dependency.rb:231:in `find_reflection'
質問
INNER JOIN か OUTER JOIN かで致命的に挙動が変わってしまうロジックを書こうとしてるのですが、そういうクエリ内容を安定させたい場合みなさんどうしてるんでしょう?
今思いついている解決案
- 素で使う時と、 merge で使う時に使う scope を使い分ける
- 使い分けるのだるいし事故りそう
- まず B の集合を求めて、それから C を求める(クエリを分割する)
- ケースによっては(今回は両方 has_one なので) B の集合が小さいのでそれでもいいかな感
- scope 内のクエリを自分で書く
scope :without_c, -> { joins("LEFT OUTER JOIN cs ON cs.b_id = bs.id").merge(C.where(id: nil)) }
- 動作の安定性では一番な気がするけど、リファクタリングと称して left_outer_joins(:c) にあっさり変えられそう。コメント必須。あとeager_loadとかいろいろ混ざって複雑なクエリになるとダメかも。
- B基準でクエリを組み立てなおす。
- 要件にもよるけど、 JOIN の階層が深くならずに済むので素直なクエリにでき、挙動の変化に悩まされることもない。Bのリストが取れるが、Aのリストが欲しい場合はBのリストから取りましょうということで。
正解?
https://gemnasium.com/gems/activerecord/versions/4.0.0 に
The solution is to build JoinAssociation when two relations with join information are being merged. And later while building the Arel use the previously built JoinAssociation record in JoinDependency#graft to build the right from clause. Fixes #3002.
と書かれているのですが、 how to build JoinAssociation がわからなかった