Rails 3.2计算没有1 + n个查询的条件关联的方法

时间:2016-02-15 06:04:20

标签: ruby-on-rails ruby-on-rails-3 activerecord

考虑以下模型:

User < ActiveRecord::Base
  has_many :posts
end

Post < ActiveRecord::Base
  belongs_to :user
end

现在,我希望在过去24小时内显示用户的帖子数量

显然,counter_cache在这里不起作用,因为我只想计算符合条件created_at > 24.hours.ago的记录

在控制器中,我会这样:

@users = User.order(:name)

在视图中我会有这个

<table>
  <tr>
    <th>Name</th>
    <th>Recent posts</th>
  </tr>
  <% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.posts.where('created_at > ?', 24.hours.ago).count %></td>
  </tr>
  <% end %>
</table>

现在这显然会对每个用户进行查询,从而导致可怕的1 + n查询问题。由于计数是有条件的,因此在控制器中添加.includes(:posts)无效。

在原始SQL中获取结果将是微不足道的。获得这些结果的正确 Rails方式是什么?最好以某种方式使用旧的3.2版本。

2 个答案:

答案 0 :(得分:1)

我建议内部加入帖子,并在 group by 的帮助下让数据库计数。然后您不需要实例化帖子。然后,SQL应如下所示:

SELECT users.*, count(posts.id) AS number_posts 
  FROM users
  LEFT OUTER JOIN posts
    ON posts.user_id = users.id
    AND posts.created_at > '2016-02-14 08:31:29' 
  GROUP BY users.id

此外,您可以利用选择,它会将计算的帖子动态添加为附加属性。您只能使用AREL来实现扩展的JOIN条件。 您应该将其推送到命名范围,例如:

User < ActiveRecord::Base
  has_many :posts
  scope :with_counted_posts(time=1.day.ago) -> {
    post_table = Post.arel_table
    join = Arel::Nodes::On.new(Arel::Nodes::Equality                            
      .new(post_table[:user_id], self.arel_table[:id])
      .and(Arel::Nodes::GreaterThan.new(post_table[:created_at], time))
    )                                                                           
    joins(Arel::Nodes::OuterJoin.new(post_table, join))              
      .group('users.id')                                               
      .select('users.*, count(posts.id) AS number_posts')  
  }
end

当然有可能进行优化和提取,但出于某些理解的原因,我做得更广泛。 然后在控制器中:

@users = User.with_counted_posts.order(:name)
users / index.html.erb 视图可能如下所示:

<table>
  <tr>
    <th>Name</th>
    <th>Recent posts</th>
  </tr>
  <% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.number_posts %></td>
  </tr>
  <% end %>
</table>

虽然我强烈建议您利用 render:collection 方法。再次 users / index.html.erb

<table>
  <tr>
    <th>Name</th>
    <th>Recent posts</th>
  </tr>
  <%= render @users %>
</table>

users / _user.html.erb partial:

  <tr>
    <td><%= user.name %></td>
    <td><%= user.number_posts %></td>
  </tr>

我还撰写了一篇关于N+1 problem and ARel

的博客文章

答案 1 :(得分:0)

我只想到两个解决方案:

解决方案1 ​​:先预先加载,然后选择带条件的结果:

在控制器中:

@users = User.includes(:posts).order(:name)

在视图中:

<% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.posts.to_a.count{ |post| post.created_at > 24.hours.ago } %></td>
  </tr>
<% end %>

解决方案2 :自定义查询:

在控制器中:

@users = User.joins("LEFT OUTER JOIN (SELECT user_id, COUNT(*) as posts_count
                                      FROM posts
                                      WHERE created_at > '#{24.hours.ago}'
                                      GROUP BY user_id
                                     ) AS temp ON temp.user_id = users.id")
             .order(:name)
             .select('users.*, COALESCE(temp.posts_count, 0) AS posts_count')

解释查询:

  • 我们使用LEFT OUTER JOIN,因为有些帖子与WHERE子句不匹配,因此它将被排除在子查询中,但在join后,它会&#39} ; s temp.posts_count将为空
  • COALESCE使用temp.posts_count,如果是nil,则会被视为0

在视图中:

<% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.posts_count %></td>
  </tr>
<% end %>

顺便说一下,1.day.ago = 24.hours.ago,我们可能会使用它,因为它是一个较短的版本:|