考虑以下模型:
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版本。
答案 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
,我们可能会使用它,因为它是一个较短的版本:|