我有以下返回预期结果的数据和SQL查询。我现在正试图在Rails中实现与Groupdate(和ActiveMedian)相同的事情。
如果我们放下Rails的一个层,并使用支持活动记录的关系代数(Arel),这是可能的。
在这种方法中,我们将自己教Arel关于date_trunc
函数*,然后为内部求和构建一个嵌套的聚合查询,不会立即执行,而是合并到外部平均聚合中:
class UserPoints
def self.averages
period = Arel::Nodes::NamedFunction.new('date_trunc', [
Arel::Nodes::Quoted.new('month'),
arel_table[:created_at]
])
points = arel_table[:points].sum
# The per-user aggregate sum subquery, as an abstract relational structure
subquery = select(points.as("points"), period.as("period")).group(:user_id, :period)
# Execute
from(subquery, quoted_table_name).group(:period).average(:points)
end
end
这种方法是通用的。它适用于具有范围关系的组合;例如,如果您想写UserPoints.where(created_at: Time.current.all_year).averages
,那么在最后一行插入适当的unscope
,变成:
from(subquery, quoted_table_name).unscope(:where).group(:period).average(:points)
类似地,要与Groupdate库结合使用,至少对于外部查询*,请尝试:
from(subquery, quoted_table_name).group_by_month(:period).average(:points)
甚至可能有机会将其重构为scope
声明,通过省略最终的聚合表达式,从而获得使用其他表达式的灵活性。
现在警告:Arel是Rails内部API,这意味着如果你想要文档,你需要阅读它的源代码,即使在小版本中也可能有破坏性的变化。这实际上是非常罕见的,如果你的代码戴上了适当的安全装置,使用Arel是可以的(很多人都这样做),这当然是一个合适的测试用例。
*我没有在内部聚合查询中使用Groupdate gem,因为它缺乏命名结果列的方法。
解决方案#2,这不是一个解决方案
这个额外的答案也使用了Arel和Groupdate gem,但它实际上比我推荐的方法更危险。我把它作为一个单独的答案,因为a)它有效,b)它看起来所以很优雅:
class UserPoints < ApplicationRecord
def self.averages_by_month
# average of points = total points / # of distinct users
points_avg = arel_table[:points].sum / arel_table[:user_id].count(true)
# execute, grouped by month
group_by_month(:created_at).calculate(:itself, points_avg)
end
end
,这给出了正确的结果!至少在编写Rails 6时是这样的。
不幸的是,骗局正在上演;这种方法依赖于更多关于活动记录内部的知识,而不是简单地使用Arel API。
讨论,或者为什么这是不好的
#calculate method's documented parameters
为:
relation.calculate(operation, column_name)
,尽管有意添加了对在聚合计算中使用Arel表达式的支持,但它没有在公共API中记录。单独来看,这可能没有那么糟糕,但是这个方法依赖于一个实现细节:Rails在调用列参数的Arel表示时,通过使用operation
参数作为方法名来构造完整的聚合表达式,从而返回聚合表达式。通过传递:itself
,我劫持了框架层之间的内部通信,并导致points_avg
在通过Kernel#itself
的内部来回期间返回自己。
这是一个元编程技巧,就像所有的特技一样,演示它很有趣,但不应该成为任何人的产品代码的一部分,至少除非有一天#calculate
方法被文档化以接受一个简单的Arel表达式,因为我们依赖于非常深入的Rails内部知识,也就是说,这是一个维护的禁止。
还有一些更相关的假设隐藏在那里,除此之外,关于分组聚合表达式求值的核心中的其他元素,例如期望列混叠只处理它所给出的任何内容。这也是可行的,所以有些人可能会说这也证明了Rails是多么健壮和通用,但它绝对是在测试合理假设的边界。
推荐总的来说,尽管这个解决方案看起来简洁而优雅,但它隐藏了比我准备推荐的用于生产的更多的魔法。相反,我把它作为一种有趣的新奇事物来呈现和解释。谁知道呢,也许有一天这甚至会得到明确的支持。