ActiveRecord:如何在最适合每个组的情况下订购和检索记录



我遇到了一个经典的最伟大的n-p-per-group问题,一只猫可以有很多小猫,但我通常只对最小的小猫感兴趣。

我已经知道如何为Cat建立scopehas_one关系。

我的问题是:有没有办法…

  1. 列出所有猫的名字以及它们最小的小猫的名字
  2. 同时按照他们最小的小猫的名字点餐

。。。在引擎盖下只使用单个SELECT

到目前为止我得到的:

class Cat < ApplicationRecord
has_many :kittens
has_one :youngest_kitten, -> { merge(Kitten.youngest) }, foreign_key: :cat_id, class_name: :Kitten
scope :with_youngest_kittens, lambda {
joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}
end
class Kitten
belongs_to :cat
scope :youngest, lambda {
joins(Kitten.younger_kittens_sql("kittens.cat_id"))
.where(younger_kittens: { id: nil })
}
def self.younger_kittens_sql(cat_field_name)
%{
LEFT OUTER JOIN kittens AS younger_kittens
ON younger_kittens.cat_id = #{cat_field_name}
AND younger_kittens.created_at > kittens.created_at
}
end
end

当我运行Cat.with_latest_kittens.order('kittens.name').map(&:name)时,一切看起来都很好:我只需一个SELECT就可以得到所有猫的名字。

但是,当我运行Cat.with_latest_kittens.order('kittens.name').map {|cat| cat.youngest_kitten.name}时,我也得到了正确的结果,但每只猫执行一个多余的额外SELECT。这是合乎逻辑的,因为with_youngest_kittens不知道它应该填充youngest_kitten。有没有办法告诉我,或者我做错了?

我认为在:with_youngest_kittens作用域中添加include将解决问题。尝试将范围更改为

scope :with_youngest_kittens, lambda {
includes(:youngest_kitten)
.joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}

这样可以防止Rails为每只小猫单独创建数据库查询。

我发现了一个不产生额外SELECT的解决方案,但它非常丑陋,所以我实际上会选择localarrow的解决方案因为它更可读!

为了完整起见,我想我还是会发布它(如果有人需要几毫秒的额外性能(:

首先,我为Cat.with_youngest_kitten范围中的每个小猫列添加定制的选择字段:

scope :with_youngest_kittens, lambda {
kitten_columns = Kitten
.column_names
.map { |column_name| "kittens.#{column_name} AS `youngest_kittens.#{column_name}`" }
.join(', ')
joins(:kittens)
.joins(Kitten.latest_outer_join_sql("cats.id"))
.where(later_kittens: { id: nil })
.select("cats.*, #{kitten_columns}")
}

然后,我用一个方法覆盖has_one youngest_kitten关系,该方法检索那些自定义选择,并在没有检索到数据的情况下调用super:

def youngest_kitten
return super if self[:'youngest_kittens.id'].nil?
kitten_hash = Hash[Kitten.column_names.collect { |column_name| [column_name, self[:"youngest_kittens.#{column_name}"]] }]
kitten_hash[:cat] = self
Kitten.new(kitten_hash)
end

最新更新