【问题标题】:In Active Record (Rails), how to select a sum of a column from a has_many relation with every parent record?在 Active Record (Rails) 中,如何从与每个父记录的 has_many 关系中选择列的总和?
【发布时间】:2016-02-05 19:58:43
【问题描述】:

考虑这个模型:

User:
  id: int

Valuable:
  id: int
  user_id: int
  value: int

用户有很多有价值的东西。

现在是这样。我想使用 ActiveRecord 选择多个用户进行任何查询,之后我希望能够看到他们所有有价值的总和,无需进行 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 个)查询,而不是实例化所有的 Valuables。我试着玩select('*, SUM(valuables.value) as total_value).joins(valuables),但没有运气。我正在使用 PostgreSQL

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

更新:抱歉,我还不清楚这一点。 (其实是我写的)。我希望所有贵重物品都被实例化。我想让 PostgreSQL 为我做计算。

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

【问题讨论】:

  • 任意 100 个随机用户?
  • 任何查询。 :) 我只是以 100 个用户为例。我希望 SUM 包含在查询中。更新了问题,谢谢。
  • 酷,total_value 是什么?
  • total_value 将是与该用户相关的所有贵重物品的“价值”字段的总和。请参阅我尝试使用的“选择”语句,我在那里定义了它。 :)
  • 您正在寻找的实际输出是什么。 .includes是fie,内部也是postgresql查询。

标签: ruby-on-rails postgresql rails-activerecord aggregate-functions


【解决方案1】:

在原始 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)> 

但是,除非您想manually load the associations,否则“default_scope”和“我仍然希望能够使用包含”的要求可能有点高。

【讨论】:

  • 这是我一直在寻找的答案。我注意到我犯的错误是选择 * 而不是 - 什么是正确的 - users.*
  • 如果您对users 有很多读取操作,但对valuables 的写入操作不多,您可能希望在has_many :valuables 关联上设置回调并使用它来更新@用户表中的 987654331@。会稍微快一点,您可以随意拉动并加入用户。
  • 这样的:UPDATE users SET total_value = val.s FROM (SELECT sum(valuables.value) AS s FROM valuables WHERE user.id = :user_id ) AS val WHERE users.id = :user_id
  • 是的,我曾经有过类似的东西。由于超出此问题范围的原因,我无法在数据库中的其他位置跟踪它并相信它总是正确的(除非我使用 VIEW 在数据库级别执行此操作,但我可以'不要写信给它)。
【解决方案2】:

方法一

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." }

【讨论】:

  • 实际上,我认为您需要 eager_loads 而不是 includes 因为包含可能不知道 LEFT JOIN 而不是单独查询。 (也许确实如此,我发现它的智能是不可预测的)。这是我得到的最接近答案的答案。我可以得到输出(这很棒),但我仍然不能直接从user 对象中得到total_value。我可以通过将哈希保存在 User 模型的类变量中来破解它(这很丑但可以工作)。我最理想的做法是使用select 将其查询到结果集中的单独行,以便 Rails 将其水合到 User 对象中。
  • @PelletenCate,我误解了。我不知道希望 total_value 成为 user 实例的属性。我已经对我的答案添加了一个编辑,应该可以完成这项工作。
  • 啊,谢谢!您刚刚在@max 之后,但我感谢您的回答和更新。我只是使用users.* 而不是使用user_columns 让它工作。按照你的方式做有什么优势吗?
  • @PelletenCate 没问题;当要连接的表有一些类似命名的列时,它可能会有所帮助。但在大多数情况下,您应该可以使用users.*
猜你喜欢
  • 2012-01-01
  • 1970-01-01
  • 2012-10-09
  • 2014-09-20
  • 1970-01-01
  • 2014-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多