【问题标题】:ActiveRecord: Exclude group if at least one record within it doesn't meet conditionActiveRecord:如果其中至少一条记录不满足条件,则排除组
【发布时间】:2015-06-29 13:49:47
【问题描述】:

我有两个模型:ownerpet。主人has_many :pets 和宠物belongs_to :owner

我想要做的是只抓住那些拥有所有重量都超过 30 磅的宠物的主人

#app/models/owner.rb
class Owner < ActiveRecord::Base
  has_many :pets
  #return only those owners that have heavy pets
end

#app/models/pet.rb
class Pet < ActiveRecord::Base
  belongs_to :owner
  scope :heavy, ->{ where(["weight > ?", 30])}
end

这是我的数据库中的内容。我有三个所有者:

  1. 尼尔,以及所有这些都是沉重的
  2. John,以及所有这些都不重
  3. Bob他的一些宠物很重一些不重

查询应该只返回 Neil。现在我的尝试返回 NeilBob

【问题讨论】:

  • 我发现在 Rails 查询中确定诸如 为什么我得到该结果 之类的最有用的资源很简单。尝试致电Owner.heavy_pets.to_sql,这应该会让您很好地了解原因。如下所示,添加 uniqdistinct 应该将结果集限制为您想要的。
  • 从描述中看不清楚您感兴趣的查询。你想要这样的东西吗? Owner.only_if_they_have_heavy_pets ?
  • @MariusPop 是的。只抓住拥有所有重型宠物的所有者。所以如果一个主人有 50 只宠物,而其中只有一只不重,那就不要抓住那个主人。如果一个主人有 50 只宠物,并且他的 50 只宠物都很重,那么抓住那个主人。
  • 而且,顺便说一句:我只想抓住那些拥有所有体重超过 30 磅的宠物的主人 - 你只想要重型狗。 尼尔,所有这些都不重。然后你写,你想归还尼尔。这是一个拼写错误,而您想要 John 吗?
  • 您使用的是哪个 RDBMS?还有,哪个 Rails 版本?

标签: sql ruby-on-rails ruby activerecord rails-activerecord


【解决方案1】:

您可以为每个owner_id 组成一个组,并检查组内的所有行是否符合要求的条件或至少有一行不匹配,您可以使用group by 实现它和having 子句:

scope :heavy, -> { group("owner_id").having(["count(case when weight <= ? then weight end) = 0", 30]) }

还有另一种选择,更多的是 Rails-ActiverRecord 方法:

scope :heavy, -> { where.not(owner_id: Pet.where(["weight <= ?", 30]).distinct.pluck(:owner_id)).distinct }

在这里你会得到所有不符合条件的owner_ids(通过矛盾搜索)并将它们从原始查询的结果中排除。

【讨论】:

    【解决方案2】:

    这不只是寻找最小宠物重量大于某个值的主人的问题吗:

    scope :heavy, -> { group("owner_id").joins(:pets).having("min(pets.weight) >= ?", 30)}
    

    或者反过来,

    scope :light, -> { group("owner_id").joins(:pets).having("max(pets.weight) < ?", 30)}
    

    顺便说一下,这些是 Owner 的作用域,而不是 Pet

    另一种方法是将其变成 Owner 的范围:

    Owner.where(Pet.where.not("pets.owner_id = owners.id and pets.weight < ?", 30).exists)
    

    略有不同,因为它检查的是不存在体重小于 30 的人,所以如果主人没有宠物,那么这个条件将匹配那个主人。

    在数据库方面,这将是对大型数据集最有效的查询。

    建议为这两种方法建立宠物索引(owner_id, weight)。

    【讨论】:

    • 您的解决方案很棒!唯一需要担心的是它复制了位于Pet 模型中的heavy 范围。有没有办法使用您的方法,但将位于Pet 模型中的heavy 范围用于minmax 聚合?我知道在这种情况下使用了merge,但是对于这个查询,我不确定如何应用它。
    • 我想不出这样做的方法,但无论如何我认为这些是根本不同的范围。我认为,如果您的目标是获得最佳查询效率(您应该这样做),那么您真正应该拥有的是一个 NOT EXISTS 相关子查询,为此您必须使用 Arel。
    【解决方案3】:

    如果你分两步做,首先你得到所有至少有 1 个重宠物的owner_ids,然后得到所有有至少 1 个不重宠物的owner_ids,然后抓住 id 存在的所有者在第一个数组中但不在第二个数组中?

    类似:

    scope :not_heavy, -> { where('weight <= ?', 30) }     
    

    ...

    owner_ids = Pet.heavy.pluck(:owner_id) - Pet.not_heavy.pluck(:owner_id)
    owners_with_all_pets_heavy = Owner.where(id: owner_ids)
    

    【讨论】:

    • 我不想为非常大的数据集这样做。
    【解决方案4】:

    您只需将uniq 添加到您的范围:

    scope :heavy_pets, -> { uniq.joins(:pets).merge(Pet.heavy) }
    

    它适用于数据库级别,使用 distinct 查询。

    【讨论】:

    • 我意识到这实际上不起作用。此查询的作用是获取所有拥有至少一只重宠物的所有者。不过,这不是我想要做的。我只想抓住那些所有相关宠物都很重的主人。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-18
    • 2015-01-25
    • 2017-11-27
    • 1970-01-01
    • 2022-10-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多