我正在尝试构建一个非常复杂的ActiveRecord查询,涉及两个联合的交集.我的解决方案有效,但生成的SQL中有很多冗余.有更有效的方法吗?
在我的宇宙中有人,猫,狗,书和专辑. (这是实际模型设置的玩具版本,它涉及更复杂的关联,因此甚至更丑陋的SQL.)
class Person < ActiveRecord::Base
has_many :cats
has_many :dogs
has_many :books
has_many :albums
...
end
我想要找回所有拥有名为琼斯的人,他们拥有一只名叫华莱士的宠物,以及一本1996年发行的书籍或专辑.我希望能够构建如下的查询:
Person.named("Jones").with_pet_named("Wallace").with_book_or_album_released_in(1996)
“命名琼斯”部分很简单:
scope :named, ->(name) { where(name: name) }
查询的另外两部分比较棘手.我现在正在这样做的方式,我正在使用Arel方法来构建联合(使用从这个谈话借来的模式:http://danshultz.github.io/talks/mastering_activerecord_arel/#/):
class Person
...
def self.with_pet_named(name)
with_cat_named = joins(:cats).where(cats: {name: name})
with_dog_named = joins(:dogs).where(dogs: {name: name})
with_cat_or_dog_named = with_cat_named.union(with_dog_named)
from(arel_table.create_table_alias(with_cat_or_dog_named, :people))
end
def self.with_book_or_album_released_in(year)
with_book_released_in = joins(:books).where(books: {release_date: year})
with_album_released_in = joins(:albums).where(albums: {release_date: year})
with_book_or_album_released_in = with_book_released_in.union(with_album_released_in)
from(arel_table.create_table_alias(with_book_or_album_released_in, :people))
end
end
当我将这些范围和方法链接在一起时,问题就出现了.检索到正确的人,但SQL看起来不是很好.
Person.named("Jones").with_pet_named("Wallace").with_book_or_album_released_in(1996).
to_sql
回报
SELECT "people".* FROM
( SELECT "people".* FROM
( SELECT "people".* FROM "people"
INNER JOIN "cats"
ON "cats"."person_id" = "people"."id"
WHERE "people"."name" = 'Jones' AND "cats"."name" = 'Wallace'
UNION
SELECT "people".* FROM "people"
INNER JOIN "dogs"
ON "dogs"."person_id" = "people"."id"
WHERE "people"."name" = 'Jones' AND "dogs"."name" = 'Wallace'
) "people"
INNER JOIN "books"
ON "books"."person_id" = "people"."id"
WHERE "people"."name" = 'Jones' AND "book"."release_date" = 1996
UNION
SELECT "people".* FROM
( SELECT "people".* FROM "people"
INNER JOIN "cats"
ON "cats"."person_id" = "people"."id"
WHERE "people"."name" = 'Jones' AND "cats"."name" = 'Wallace'
UNION
SELECT "people".* FROM "people"
INNER JOIN "dogs"
ON "dogs"."person_id" = "people"."id"
WHERE "people"."name" = 'Jones' AND "dogs"."name" = 'Wallace'
) "people"
INNER JOIN "albums"
ON "albums"."person_id" = "people"."id"
WHERE "people"."name" = 'Jones' AND "album"."release_date" = 1996
) "people"
WHERE "people"."name" = 'Jones'
我想我想要更像这样的东西:
SELECT "people".* FROM
( ( SELECT "people".* FROM "people"
INNER JOIN "cats"
ON "cats"."person_id" = "people"."id"
WHERE "cats"."name" = 'Wallace'
UNION
SELECT "people".* FROM "people"
INNER JOIN "dogs"
ON "dogs"."person_id" = "people"."id"
WHERE "dogs"."name" = 'Wallace'
) INTERSECT
( SELECT "people".* FROM "people"
INNER JOIN "books"
ON "books"."person_id" = "people"."id"
WHERE "book"."release_date" = 1996
UNION
SELECT "people".* FROM "people"
INNER JOIN "albums"
ON "albums"."person_id" = "people"."id"
WHERE "album"."release_date" = 1996
)
) "people"
WHERE "people"."name" = 'Jones'
虽然笨拙,但至少不是多余的.有没有办法使用ActiveRecord和Arel方法获得更简洁的SQL查询,而无需更改我的模型或关联?
最佳答案 没有你的数据库架构,很难测试这个,但我认为这应该让你更接近你想要的SQL.
注意:此“解决方案”会破坏代码中可用的方法链.
class Person
#…
def self.with_pet_named(name)
with_cat_named = joins(:cats).where(cats: {name: name})
with_dog_named = joins(:dogs).where(dogs: {name: name})
Arel::Nodes::Union.new(with_cat_named.ast, with_dog_named.ast)
end
def self.with_book_or_album_released_in(year)
with_book_released_in = joins(:books).where(books: {release_date: year})
with_album_released_in = joins(:albums).where(albums: {release_date: year})
Arel::Nodes::Union.new(with_book_released_in.ast, with_album_released_in.ast)
end
#…
end
inter = Arel::Nodes::Intersect.new(
Person.with_pet_named('Wallace'),
Person.with_book_or_album_released_in(1996)
)
p = Arel::Nodes::TableAlias.new(inter, :p)
Person.from(p).where(p[:name].eq('Jones')).to_sql