rails 范围检查关联是不是不存在
Posted
技术标签:
【中文标题】rails 范围检查关联是不是不存在【英文标题】:rails scope to check if association does NOT existrails 范围检查关联是否不存在 【发布时间】:2012-05-08 11:01:58 【问题描述】:我希望编写一个返回所有没有特定关联的记录的范围。
foo.rb
class Foo < ActiveRecord::Base
has_many :bars
end
bar.rb
class Bar < ActiveRecord::Base
belongs_to :foo
end
我想要一个可以找到所有Foo's
的范围,不 有任何bars
。使用joins
很容易找到具有关联的那些,但我还没有找到相反的方法。
【问题讨论】:
【参考方案1】:Rails 4 让这变得太容易了 :)
Foo.where.not(id: Bar.select(:foo_id).uniq)
这会输出与 jdoe 的答案相同的查询
SELECT "foos".*
FROM "foos"
WHERE "foos"."id" NOT IN (
SELECT DISTINCT "bars"."foo_id"
FROM "bars"
)
作为一个范围:
scope :lonely, -> where.not(id: Bar.select(:item_id).uniq)
【讨论】:
我不得不使用.pluck(:foo_id)
而不是.select(:foo_id)
。你的方法对我不起作用,因为在模型上调用 .select
会返回一个 ActiveRecord_Relation
而不仅仅是数组中的 id
s——这就是你想要的 Model.where.not(id: [1, 2, ...]
语句,对吧?
是的,.select
返回一个 ActiveRecord 关系 .pluck
返回一个 Ruby 数组。但是当使用.select
作为子查询添加时,您只需调用一次数据库,因为rails 会自动将它与其他查询结合起来。使用.pluck
,您将调用数据库两次。此外,请记住,作用域通常链接在一起,因此不建议使用.pluck
。
也许它对我不起作用的原因是因为我现在正在使用 SQLite(我正在开发一个小程序)。
是的,可能是这样,如果它是一个小项目,两次调用数据库不会是一个大问题:)
在 Rails 5 中,uniq
被删除以支持 distinct
,因此它不再覆盖 Ruby 的原生 uniq
。【参考方案2】:
适用于 Rails 5+(Ruby 2.4.1 和 Postgres 9.6)
我有 100 个foos
和 9900 个bars
。 99 个foos
每个有 100 个bars
,其中一个没有。
Foo.left_outer_joins(:bars).where(bars: foo_id: nil )
产生一个 SQL 查询:
Foo Load (2.3ms) SELECT "foos".* FROM "foos" LEFT OUTER JOIN "bars" ON "bars"."foo_id" = "foos"."id" WHERE "bars"."foo_id" IS NULL
并返回没有bars
的Foo
当前接受的答案Foo.where.not(id: Bar.select(:foo_id).uniq)
是不工作。它正在生成两个 SQL 查询:
Bar Load (8.4ms) SELECT "bars"."foo_id" FROM "bars"
Foo Load (0.3ms) SELECT "foos".* FROM "foos" WHERE ("foos"."id" IS NOT NULL)
它返回所有foos
,因为所有foos
都有一个不为空的id
。
需要将其更改为Foo.where.not(id: Bar.pluck(:foo_id).uniq)
以将其减少为一个查询并找到我们的Foo
,但它在基准测试中表现不佳
require 'benchmark/ips'
require_relative 'config/environment'
Benchmark.ips do |bm|
bm.report('left_outer_joins') do
Foo.left_outer_joins(:bars).where(bars: foo_id: nil )
end
bm.report('where.not') do
Foo.where.not(id: Bar.pluck(:foo_id).uniq)
end
bm.compare!
end
Warming up --------------------------------------
left_outer_joins 1.143k i/100ms
where.not 6.000 i/100ms
Calculating -------------------------------------
left_outer_joins 13.659k (± 9.0%) i/s - 68.580k in 5.071807s
where.not 70.856 (± 9.9%) i/s - 354.000 in 5.057443s
Comparison:
left_outer_joins: 13659.3 i/s
where.not: 70.9 i/s - 192.77x slower
【讨论】:
感谢您的精彩提示! 很棒的提示!你能用“WHERE NOT EXISTS”变体详细说明吗?【参考方案3】:在 foo.rb 中
class Foo < ActiveRecord::Base
has_many :bars
scope :lonely, lambda joins('LEFT OUTER JOIN bars ON foos.id = bars.foo_id').where('bars.foo_id IS NULL')
end
【讨论】:
由于has_many :bars
是在Foo
上定义的,我认为您可以跳过明确的joins(...)
在您的范围内使用includes
,这最终会产生相同的结果:@ 987654326@【参考方案4】:
我更喜欢使用squeel gem 来构建复杂的查询。它以这样的魔力扩展了 ActiveRecord:
Foo.whereid.not_in Bar.selectfoo_id.uniq
构建以下查询:
SELECT "foos".*
FROM "foos"
WHERE "foos"."id" NOT IN (
SELECT DISTINCT "bars"."foo_id"
FROM "bars"
)
所以,
# in Foo class
scope :lonely, whereid.not_in Bar.selectfoo_id.uniq
是你可以用来构建请求的范围。
【讨论】:
为什么?这是一个单一的 SQL 查询,它的嵌套部分应该非常快。 是否没有内置的 AR 方式来完成类似的查询? Nope :( 你必须使用文本 sn-ps 来编写NOT IN
之类的东西并将它们与 Ruby 代码组合在一起。这种组合看起来很难看。比较:Product.where('price < ?', some_price)
(AR) 与 @ 987654327@(尖叫声)。
说有数以百万计的酒吧。您正在强制对该表进行全面扫描(尽管我认为数据库可能会重写为连接)如果 foo_id 列上没有索引,那么 distinct 也会受到伤害
你真的认为某些魔法加入之王可以消除你所有酒吧的搜索吗? :)【参考方案5】:
将 NOT EXISTS 与 LIMIT-ed 子查询一起使用会更快:
SELECT foos.* FROM foos
WHERE NOT EXISTS (SELECT id FROM bars WHERE bars.foo_id = foos.id LIMIT 1);
使用 ActiveRecord (>= 4.0.0):
Foo.where.not(Bar.where("bars.foo_id = foos.id").limit(1).arel.exists)
【讨论】:
【参考方案6】:此方法利用includes
并允许范围链接。它应该适用于 Rails 5+
scope :barless, ->
includes(
:bars
).where(
bars:
id: nil
)
【讨论】:
以上是关于rails 范围检查关联是不是不存在的主要内容,如果未能解决你的问题,请参考以下文章
如何在执行 rake db:setup 之前检查数据库是不是存在于 Rails 中