问题 Rails Arel选择不同的列


我用新手来轻微阻挡 scope 方法(Arel 0.4.0,Rails 3.0.0.rc)

基本上我有:

一个 topics 模特,哪个 has_many :comments,和 comments 模型(用 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 字段,显示最后一篇帖子多久以前?我会想象一些类似的东西 distinct 要么 group_by,但我不太清楚如何最好地去做。

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


9401
2017-08-24 08:33


起源



答案:


解决方案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

确保你索引 created_at 和 topic_id 列中的 comments 表。

解决方案2

添加一个 last_comment_id 你的专栏 Topic 模型。更新 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查询以确定热门主题更有效。


5
2017-08-27 05:14



谢谢你的回答 - 这是一个解决方案,但我真的只是在寻找一个使用Rails3 / Arel的解决方案 scope! - Jeriko
更新了我的回答,看一看。 - Harish Shetty
我正在接受这个答案,并向你颁发赏金 - 这不是我最初的问题所回答的问题,但无论如何它确实更好用。谢谢 :) - Jeriko
将交易用于此类事情是否过度? - Dex
这样做效果更好,但可以在Rails 3.2.1中使用.uniq,参见 apidock.com/rails/ActiveRecord/QueryMethods/uniq - ice cream


答案:


解决方案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

确保你索引 created_at 和 topic_id 列中的 comments 表。

解决方案2

添加一个 last_comment_id 你的专栏 Topic 模型。更新 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查询以确定热门主题更有效。


5
2017-08-27 05:14



谢谢你的回答 - 这是一个解决方案,但我真的只是在寻找一个使用Rails3 / Arel的解决方案 scope! - Jeriko
更新了我的回答,看一看。 - Harish Shetty
我正在接受这个答案,并向你颁发赏金 - 这不是我最初的问题所回答的问题,但无论如何它确实更好用。谢谢 :) - Jeriko
将交易用于此类事情是否过度? - Dex
这样做效果更好,但可以在Rails 3.2.1中使用.uniq,参见 apidock.com/rails/ActiveRecord/QueryMethods/uniq - ice cream


在大多数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]}

3
2017-08-24 09:55



谢谢 - 这绝对是一个解决方案,但它并不是很理想。主题以任意顺序返回,我将无法访问最后一条评论的时间 - 因此我尝试加入表。是否无法将原始查询分组以返回不同的结果?我觉得它几乎就在那里,只是不完全。 - Jeriko
啊,那我最多只解决了你问题的一半。最简单的方法可能是获取最新独特主题的所有评论,然后只显示最新的。执行此操作的SQL通常与供应商特定的解决方案相关。最有可能是ANSI SQL这样做的方式,但我实际上怀疑Arel是否支持它。希望我错了。 - Jonas Elfström


为了实现这一点,你需要有一个范围 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')
 => #<Comment id: 101, subject: "Topic 1 - New comment", topic_id: 1, content: nil, created_at: "2010-08-26 10:53:34", updated_at: "2010-08-26 10:53:34"> 
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

3
2017-08-26 10:57



太棒了 - 我还没来得及测试它,但它看起来很完美。我在开发中使用sqlite,但在生产中可能是mysql,因此我将不得不测试它的翻译方式 - 很快就会回复。 - Jeriko
您将如何获得此方法中主题的最后评论的created_date? - Harish Shetty
似乎减少的GROUP BY语法是MySQL特定的 dev.mysql.com/tech-resources/articles/... - Jonas Elfström
你的答案肯定对我很有帮助 - 但我最终选择了KandadaBoggu的解决方案,所以我认为授予他赏金是公平的!谢谢! - Jeriko
没问题,很高兴它有所帮助 - Steve Weet


由于这个问题是关于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


2
2018-04-03 16:09