一次查询ActiveRecord以进行记录和关系计算

时间:2014-10-26 20:28:50

标签: mysql ruby-on-rails ruby activerecord ruby-on-rails-4

TL; DR?请参见编辑2

我有一个小Rails应用程序,有一些人可以玩的不同类型的游戏:它基于体育,所以他们可以每周挑选每个游戏的获胜者(模型PickEm,属性{{ 1}} boolean与correct用于未完成的游戏),并预测特定团队游戏的结果(模型nil,属性Guess带整数,score表示未完成的游戏) 。每nil User has_manyPickEms。我正在尝试显示排名(Guesses /总计 - 总计全部为非correctnil /总数可能。)

我发现我可以收集用户及其相关记录,但在尝试显示排名时,我发现每个用户都在触发另一个查询 - 随着用户群的增加而缓慢且不可持续。那是因为score@user.pick_em_scorepick_ems.where(correct: true).size@user.guess_Score。所以我调用guesses.where.not(score: nil).sum(:score)并运行该查询。我觉得应该有办法同时获得每个user.pick_em_score以及这些特定的计数,而不是缓冲一大堆不必要的额外内容。

我需要什么:

  • User记录
  • User(通过计算User.pick_em_score条记录计算)
  • correct计算User.pick_ems
  • 的位置
  • NOT NULL(由User.guesses_score计算)
  • guesses.sum(:score)计算User.guesses
  • 的位置

我在Rails的ActiveRecord帮助程序中找到的大部分内容,尤其是与计算相关的内容,仅用于检索 计算。看起来我可能需要直接钻研NOT NULL等等。但我无法让它发挥作用。有人能指出我正确的方向吗?

修改

澄清:我知道我可以将此信息写入select()模型,但这个限制性过于严格:下一季,我需要在User添加新列 年度的结果等。此外,这是第三级回调更新相关模型 - User模型已更新相关的MatchPickEms保存。 我正在寻找最简单的ActiveRecord查询或查询,以便能够使用此信息,如标题所示。理想情况下,一个查询返回上述信息,但如果需要的话,没关系。

我曾经在PHP中使用MySQL直接工作,但是那些技能已经生锈了(在原始的MySQL中,我想,我会有几个子选择语句来帮助拉动这些计数)而且我也喜欢能够使用Rails的ActiveRecord助手等,并尽可能避免构建原始SQL。

第二次编辑:

我似乎已将其归结为启动工作的一个调用,但我正在编写大量SQL。它也很脆弱,IMO,并试图用它运行失败了。看起来我只是将Rails中的百万个奇异Guesses查询直接推送到SQL中,但这可能仍然是一个进步。

SELECT

问题似乎是:有没有办法使用Rails而不是原始SQL来链接我们在那里看到的User.unscoped.select('users.*', '(SELECT COUNT(*) FROM pick_ems WHERE pick_ems.user_id = users.id AND pick_ems.correct) AS correct_pick_ems', '(SELECT COUNT(*) FROM pick_ems WHERE pick_ems.user_id = users.id AND pick_ems.correct IS NOT NULL) AS total_pick_ems', '(SELECT SUM(guesses.score) FROM guesses WHERE guesses.user_id = users.id AND guesses.score IS NOT NULL) AS guesses_score', '(SELECT COUNT(*) FROM guesses WHERE guesses.user_id = users.id AND guesses.score IS NOT NULL) AS guesses_count' ) 这些子查询?或者只是......一般来说,更好的方法来构建它?

此外,我正在为users.id运行另一组SELECT,这将取决于WHEREtotal_pick_ems guesses_count,但是我不能使用那些别名列,我必须再次调用> 0

4 个答案:

答案 0 :(得分:1)

欢迎来到AR。它真的只适用于简单的CRUD,比如查询。一旦你真的想要在愤怒中查询你的数据,它就没有能力去做你想要的查询而不需要求助于批量的SQL字符串,而且往往放弃了链接的能力。

正是为什么我转向Sequel,因为它具有使用更全面的SQL功能集组成查询的功能,包括连接条件,窗口函数,递归公用表表达式和高级预先加载。与AR和Arel相比,作者非常敏感,文档也很出色。

我不希望你会喜欢这个答案,但是当你开始看到铁轨附带的自以为是的组件之外的时候会到来,我不得不说这些组件并不是最好的。 Sequel也加倍了我的应用程序,因为我可以通过AR得到它,不仅仅是开发人员的幸福,它意味着更少的服务器运行。是的,这将是一个学习曲线,但IMO更好地学习有背后的工具。

答案 1 :(得分:1)

加入可能会奏效。如下所示

User.unscoped.joins(:guesses).joins(:pick_ems).
where("guesses.score IS NOT NULL").
select("users.*, 
sum(guesses.score) as guesses_score,
count(guesses.id) as guesses_count,
count(case when pick_ems.correct = True then 1 else null end) 
as correct_pick_ems,
count(case when pick_ems.correct != null then 1 else null end)
as total_pick_ems,
").
group("users.id")

如果您需要一次有限数量的用户使用此信息,那么使用类方法(例如

)进行查询或预先加载(User.includes(:guesses, :pick_ems)
def correct_pick_ems
   pick_ems.count(&:correct)
end

会起作用。

但是,如果您在大多数时间内需要所有用户使用此信息,那么users表中的缓存计数器将更加优化。

答案 2 :(得分:0)

你需要的是某种自定义(智能)counter_cache只能在某些条件下计数(例如是正确的)

您可以使用conditional after_save& after_destroy触发器构建您自己的自定义counter_cache,如下所示:

class PickEm
  belongs_to :user

  after_save :increment_finished_counter_cache, if: Proc.new { |pick_em| pick_em.correct }
  after_destroy :decrement_finished_counter_cache, if: Proc.new { |pick_em| pick_em.correct }

  private

  def increment_finished_counter_cache
    self.user.update_column(:finished_games_counter, self.user.finished_games_counter + 1) #update_column should not trigger any validations or callbacks
  end

  def decrement_finished_counter_cache
    self.user.update_column(:finished_games_counter, self.user.finished_games_counter - 1) #update_column should not trigger any validations or callbacks
  end
end

注意:

  • 未经过测试的代码(仅用于表明想法)

  • 有些人说最好避免将自定义计数器命名为rails命名(foo_counter_cache)

答案 3 :(得分:0)

你应该对它进行基准测试,但我的预感是,将所有数据添加到单个SELECT并不比将其分解为单独的SELECT更快(I&I #39;实际上有后者更快的情况)。通过分解,您还可以使用更多的ActiveRecord和更少的原始SQL,例如:

user_ids_to_pick_em_score = User.joins(:pick_ems).where(pick_ems: {correct: true}).group(:user_id).count
user_ids_to_pick_ems_count = User.joins(:pick_ems).where.not(pick_ems: {correct: nil}).group(:user_id).count
user_ids_to_guesses_score = Hash[User.select("users.id, SUM(guesses.score) AS total_score").joins(:guesses).group(:user_id).map{|u| [u.id, u.total_score]}]
user_ids_to_guesses_count = User.joins(:guesses).where.not(guesses: {score: nil}).group(:user_id).count

修改:要显示它们,您可以这样做:

<%- User.select(:id, :name).find_each do |u| -%>
  Name: <%= u.name %>
  Picks Correct: <%= user_ids_to_pick_em_score[u.id] %>/<%= user_ids_to_pick_ems_count[u.id] %>
  Total Score: <%= user_ids_to_guesses_score[u.id] %>/<%= user_ids_to_guesses_count[u.id] %>
<%- end -%>