在 Active Record (Rails) 中,如何从与每个父记录的 has_many 关系中 select 列的总和?

In Active Record (Rails), how to select a sum of a column from a has_many relation with every parent record?

考虑这个模型:

User:
  id: int

Valuable:
  id: int
  user_id: int
  value: int

用户有很多贵重物品。

事情是这样的。我想使用 ActiveRecord select 多个用户进行任何查询,之后我希望能够看到他们所有有价值的东西的总和,而不必进行 N+1 查询。

所以,我希望能够做到这一点:

# @var ids [Array] A bunch of User IDs.
User.where(id: ids).each do |u| 
  puts "User ##{u.id} has total value of #{u.total_value}
end

它应该执行 1(或最多 2)次查询,而不是实例化所有值。我试着玩 select('*, SUM(valuables.value) as total_value).joins(valuables),但没有运气。我正在使用 PostgreSQL

如果可能的话,我希望它自动发生(例如使用 default_scope)并且我仍然希望能够使用 includes.

更新: 抱歉,我没说清楚。 (实际上,我确实写了它)。我希望实例化所有有价值的东西。我想让 PostgreSQL 帮我计算。

更新: 我的意思是,我想依靠 PostgreSQL 的 SUM 方法来获得结果集中的总和。我认为我在使用 SELECTGROUP BY 时所做的努力使这一点变得清晰。我不希望 Valuables table 中的任何数据或记录对象成为结果的一部分,因为它消耗太多内存并且使用 Ruby 计算字段只会使用太多 CPU 并且时间太长了。

方法一

hash = User.joins(:valuables).group('users.id').sum('valuables.value')
hash.each { |uid, tval| puts "User ID #{uid} has total value #{tval}." }

请注意,哈希将只包含有贵重物品的用户的条目。要获得所有用户(即使是那些没有贵重物品的用户),您可以使用 includes 而不是 joins

方法二

value_hash = Valuable.group(:user_id).sum(:value)
all_user_ids = User.pluck(:id)

all_user_ids.each do |uid| 
  tval = value_hash[uid] || 0
  puts "User ID #{uid} has total value #{tval}."
end

我会选择 方法 1includes

编辑: 更好地理解问题后:

user_columns = User.column_names.join(', ')

users = User.
        includes(:valuables).
        group('users.id').
        select("#{user_columns}, sum(valuables.value) AS total_value")

users.each { |u| puts "User ID #{user.id} has #{u.total_value} total value." }

原始 SQL 你想要这样的东西:

SELECT users.*, SUM(valuable.value) AS values_sum
FROM users
LEFT OUTER JOIN valuables ON users.id = valuables.user_id
GROUP BY users.id

所以在 ActiveRecord 查询中翻译它应该是这样的:

User.select('users.*, SUM(valuables.value) AS total_value')
    .joins('LEFT OUTER JOIN valuables ON users.id = valuables.user_id')

请注意,您实际上并未从 valuables 中选择任何列。

[10] pry(main)> @users = User.select('users.*, SUM(valuables.value) AS total_value').joins('LEFT OUTER JOIN valuables ON users.id = valuables.user_id')

  User Load (1.4ms)  SELECT users.*, SUM(valuables.value) as total_value FROM "users" LEFT OUTER JOIN valuables ON users.id = valuables.user_id
=> [#<User:0x007f96f7dff6e8
  id: 8,
  created_at: Fri, 05 Feb 2016 20:36:34 UTC +00:00,
  updated_at: Fri, 05 Feb 2016 20:36:34 UTC +00:00>]
[11] pry(main)> @users.map(&:total_value)
=> [6]
[12] pry(main)> 

但是 "default_scope" 和 "I still want to be able to use includes" 的要求可能有点高,除非你想 manually load the associations