【问题标题】:Rails Query on dynamic propertyRails 查询动态属性
【发布时间】:2021-06-15 15:57:24
【问题描述】:

我正在构建一个小测验测试应用程序,它允许每个用户参加多个测验,然后为每个测验给出 10 分。

所以一个用户has_many :quizes

并且每个测验都有一个 10 分,用户也可以看到他们的平均分数,这是在用户每次提出请求时动态计算的。这是通过用户模型上的方法完成的

def avg_score
  self.quizes.pluck(:score).sum/self.quizes.count
end

对于应用程序的主屏幕,我想显示平均得分高于 8 的所有用户。

无论如何我可以查询这个动态值吗? 还是在 users 表中创建一个字段,然后在每次测验完成时更新值是更好的方法?

【问题讨论】:

    标签: ruby-on-rails ruby ruby-on-rails-5


    【解决方案1】:

    无论如何我可以查询这个动态值吗?还是在用户表中创建一个字段,然后在每次测验完成时更新值是否更好?

    是的,如果您将 ActiveRecord 与 SQL 数据库一起使用,这是可能的。您可以在 SQL 中使用 group by 按用户 ID 对每个测验进行分组,然后使用 avg 返回平均分数,然后使用 where 将列表限制为平均分数超过 8 的用户。

    SELECT user_id, AVG(score)
    FROM quizzes
    WHERE AVG(score) > 8
    GROUP BY user_id
    

    在 Rails 中应该是这样的:

    Quiz.select(
      :user_id,
      Quiz.arel_table[:score].sum
    ).where(
      Quiz.arel_table[:score].sum.gt(8)
    ).group(:user_id)
    

    【讨论】:

    • 一些注意事项:1) SUM != AVG(您可以将其更改为Quiz.arel_table[:score].average。2)您不能在 WHERE 子句中使用聚合函数,我们需要将其移动到 HAVING 例如.having(Quiz.arel_table[:score].sum.gt(8)) 3) 这将返回 Quiz 对象,而我相信 OP 正在寻找 User 对象。
    【解决方案2】:

    是的,您可以使用数据库来完全处理这个过程。

    首先:Sum/Count = Average 所以quizes.average(:score) 在这里会是一个更好的实现

    也就是说,如果您希望所有用户的平均得分超过 8,我们不应该逐个实例地执行此操作,而是将其放入模型中并执行查询以返回这些用户,例如

    class User < ApplicationRecord
      has_many :quizes 
      scope :over_score_of, ->(score) { 
        select(User.arel_attribute(Arel.star),
               Quiz.arel_attribute(:score).average.as('average_score'))
               .joins(:quizes)
               .group(:id)
               .having(Quiz.arel_attribute(:score).average.gt(score))
      } 
    end 
    

    然后你可以在整个应用程序中使用它

    @users = User.over_score_of(8)
    

    您可以在虚拟属性average_score 中访问每个用户的平均分数,而无需执行额外的查询。

    生成的 SQL 类似于

    SELECT 
      "users".*,
      AVG("quizes"."score") AS average_score
    FROM 
      "users" 
      INNER JOIN "quizes" ON "quizes"."user_id" = "users"."id" 
    GROUP BY 
      "users"."id" 
    HAVING 
       AVG("quizes"."score") > 8 
    

    注意事项:

    • 某些数据库(例如 MSSQL)不支持仅按主键进行分组,因此可能需要对实现进行一些更改
    • 根据分数的数据类型,“平均值”可能会返回一个整数,即使您需要一个十进制值(需要进行强制转换)

    【讨论】:

      【解决方案3】:

      已经有一些很好的答案,为了完整起见,仅仅因为我并不爱上 Arel(imo 的可读性很差),你可以考虑使用普通的旧 SQL 片段。

          User.select("users.*", "avg(quizzes.score) as average_score",
             .joins(:quizzes)
             .group('users.id')
             .having("avg(quizzes.score) > ?", 8))
      

      我认为这很容易,您不必介绍 Arel - 它更接近实际的 sql 查询,因此 imo 更具可读性;缺点是您必须引用表并且理论上编写不可移植的 sql 片段,但这对我来说似乎很简单。

      【讨论】:

        猜你喜欢
        • 2014-09-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-13
        • 2011-06-12
        • 2018-12-08
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多