Rails Arel选择不同的列

我使用新的scope方法(Arel 0.4.0,Rails 3.0.0.rc)轻微阻止了

基本上我有:

topics模型, has_many :commentscomments模型(带有topic_id列),其belongs_to :topics

我正在尝试获取“热门话题”的集合,即最近评论过的主题。 目前的代码如下:

 # models/comment.rb scope :recent, order("comments.created_at DESC") # models/topic.rb scope :hot, joins(:comments) & Comment.recent & limit(5) 

如果我执行Topic.hot.to_sql ,则会触发以下查询:

 SELECT "topics".* FROM "topics" INNER JOIN "comments" ON "comments"."topic_id" = "topics"."id" ORDER BY comments.created_at DESC LIMIT 5 

这样可以正常工作,但它可能会返回重复的主题 – 如果主题#3最近被多次评论过,则会多次返回。

我的问题

我将如何回归一组不同的主题,记住我仍然需要访问comments.created_at字段,以显示上一篇文章的持续时间? 我会想象一下像distinctgroup_by这样的东西,但我不太清楚如何最好地去做。

非常感谢任何建议/建议 – 我已经添加了100个代表奖金,希望很快能找到一个优雅的解决方案。

解决方案1

这不使用Arel,而是使用Rails 2.x语法:

 Topic.all(:select => "topics.*, C.id AS last_comment_id, C.created_at AS last_comment_at", :joins => "JOINS ( SELECT DISTINCT A.id, A.topic_id, B.created_at FROM messages A, ( SELECT topic_id, max(created_at) AS created_at FROM comments GROUP BY topic_id ORDER BY created_at LIMIT 5 ) B WHERE A.user_id = B.user_id AND A.created_at = B.created_at ) AS C ON topics.id = C.topic_id " ).each do |topic| p "topic id: #{topic.id}" p "last comment id: #{topic.last_comment_id}" p "last comment at: #{topic.last_comment_at}" end 

确保索引comments表中的created_attopic_id列。

解决方案2

Topic模型中添加last_comment_id列。 创建评论后更新last_comment_id 。 此方法比使用复杂SQL确定最后一条注释要快得多。

例如:

 class Topic < ActiveRecord::Base has_many :comments belongs_to :last_comment, :class_name => "Comment" scope :hot, joins(:last_comment).order("comments.created_at DESC").limit(5) end class Comment belongs_to :topic after_create :update_topic def update_topic topic.last_comment = self topic.save # OR better still # topic.update_attribute(:last_comment_id, id) end end 

这比运行复杂的SQL查询以确定热门主题更有效。

在大多数SQL实现中,这并不是那么优雅。 一种方法是首先获取按topic_id分组的五个最新评论的列表。 然后通过使用IN子句进行子选择来获取comments.created_at。

我对Arel很新,但这样的事情可行

 recent_unique_comments = Comment.group(c[:topic_id]) \ .order('comments.created_at DESC') \ .limit(5) \ .project(comments[:topic_id] recent_topics = Topic.where(t[:topic_id].in(recent_unique_comments)) # Another experiment (there has to be another way...) recent_comments = Comment.join(Topic) \ .on(Comment[:topic_id].eq(Topic[:topic_id])) \ .where(t[:topic_id].in(recent_unique_comments)) \ .order('comments.topic_id, comments.created_at DESC') \ .group_by(&:topic_id).to_a.map{|hsh| hsh[1][0]} 

为了实现这一点,您需要使用GROUP BY作为范围来获取每个主题的最新注释。 然后,您可以通过created_at对此范围进行排序,以获取最新的主题评论。

以下适用于我使用sqlite

 class Comment < ActiveRecord::Base belongs_to :topic scope :recent, order("comments.created_at DESC") scope :latest_by_topic, group("comments.topic_id").order("comments.created_at DESC") end class Topic < ActiveRecord::Base has_many :comments scope :hot, joins(:comments) & Comment.latest_by_topic & limit(5) end 

我使用以下seeds.rb来生成测试数据

 (1..10).each do |t| topic = Topic.new (1..10).each do |c| topic.comments.build(:subject => "Comment #{c} for topic #{t}") end topic.save end 

以下是测试结果

 ruby-1.9.2-p0 > Topic.hot.map(&:id) => [10, 9, 8, 7, 6] ruby-1.9.2-p0 > Topic.first.comments.create(:subject => 'Topic 1 - New comment') => # ruby-1.9.2-p0 > Topic.hot.map(&:id) => [1, 10, 9, 8, 7] ruby-1.9.2-p0 > 

为sqlite(重新格式化)生成的SQL非常简单,我希望Arel会为其他引擎呈现不同的SQL,因为在主题中的列不在“按列表分组”中,这肯定会在许多数据库引擎中失败。 如果这确实存在问题,那么您可以通过将所选列限制为comments.topic_id来克服它

 puts Topic.hot.to_sql SELECT "topics".* FROM "topics" INNER JOIN "comments" ON "comments"."topic_id" = "topics"."id" GROUP BY comments.topic_id ORDER BY comments.created_at DESC LIMIT 5 

由于问题是关于Arel的,我想我会添加它,因为Rails 3.2.1将uniq添加到QueryMethods:

如果将.uniq添加到Arel,则会将DISTINCT添加到select语句中。

例如Topic.hot.uniq

也适用于范围:

例如scope :hot, joins(:comments).order("comments.created_at DESC").limit(5).uniq

所以我会假设

 scope :hot, joins(:comments) & Comment.recent & limit(5) & uniq 

也应该工作。

请参阅http://apidock.com/rails/ActiveRecord/QueryMethods/uniq