【问题标题】: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 很容易找到具有关联的那些,但我还没有找到相反的方法。

【问题讨论】:

    标签: sql ruby-on-rails associations scopes


    【解决方案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 而不仅仅是数组中的 ids——这就是你想要的 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
    

    并返回没有barsFoo

    当前接受的答案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.where{id.not_in Bar.select{foo_id}.uniq}
    

    构建以下查询:

    SELECT "foos".* 
    FROM "foos" 
    WHERE "foos"."id" NOT IN (
      SELECT DISTINCT "bars"."foo_id"
      FROM "bars" 
    )
    

    所以,

    # in Foo class
    scope :lonely, where{id.not_in Bar.select{foo_id}.uniq}
    

    是你可以用来构建请求的范围。

    【讨论】:

    • 为什么?这是一个单一的 SQL 查询,它的嵌套部分应该非常快。
    • 是否没有内置的 AR 方式来完成类似的查询?
    • Nope :( 你必须使用文本 sn-ps 来编写 NOT IN 之类的东西并将它们与 Ruby 代码组合在一起。这种组合看起来很难看。比较:Product.where('price &lt; ?', 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
          }
        )
      }
      

      【讨论】:

        猜你喜欢
        • 2012-05-30
        • 1970-01-01
        • 2017-12-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-06-09
        相关资源
        最近更新 更多