【问题标题】:Rails Arel selecting distinct columnsRails Arel 选择不同的列
【发布时间】:2011-04-03 01:18:36
【问题描述】:

我在使用新的 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 代表的赏金,希望尽快找到一个优雅的解决方案。

【问题讨论】:

    标签: ruby-on-rails activerecord arel


    【解决方案1】:

    解决方案 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 查询来确定热点话题要高效得多。

    【讨论】:

    • 感谢您的回答 - 这是一个解决方案,但我真的只是在寻找一个使用 Rails3/Arel scope 的解决方案!
    • 更新了我的答案,看看吧。
    • 我接受这个答案并将赏金奖励给你 - 它没有回答我最初的问题,但无论如何它实际上效果更好。谢谢:)
    • 在这类事情上使用事务会不会过大?
    • 这样效果更好,但可以在 Rails 3.2.1 中使用 .uniq,参见 apidock.com/rails/ActiveRecord/QueryMethods/uniq
    【解决方案2】:

    这在大多数 SQL 实现中并不那么优雅。一种方法是首先获取按 topic_id 分组的五个最近的 cmets 的列表。然后使用 IN 子句通过子选择获取 cmets.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]}
    

    【讨论】:

    • 谢谢 - 这绝对是一个解决方案,但不是很理想。主题以任意顺序返回,我将无法访问最后评论的时间 - 因此我尝试加入表。有没有办法对我的原始查询进行分组以返回不同的结果?我觉得它快到了,只是不完全。
    • 啊,那我最多只解决了你的问题的一半。最简单的方法可能是获取所有最近的独特主题的 cmets,然后只显示最新的。执行此操作的 SQL 经常使用供应商特定的解决方案。很可能有一种 ANSI SQL 方法可以做到这一点,但我实际上怀疑 Arel 是否支持它。希望我错了。
    【解决方案3】:

    为了实现这一点,您需要有一个带有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(reformatted) 生成的 SQL 非常简单,我希望 Arel 能够为其他引擎呈现不同的 SQL,因为这在许多数据库引擎中肯定会失败,因为 Topic 中的列不在“Group by list”中。如果这确实存在问题,那么您可以通过将选定的列限制为 cmets.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
    

    【讨论】:

    • 太棒了 - 我还没有时间测试它,但它看起来很完美。我在开发中使用 sqlite,但在生产中可能使用 mysql,所以我必须测试它是如何翻译的 - 很快就会回复。
    • 在这种方法中,您将如何获取某个主题的最后一条评论的 created_date?
    • 看来简化的 GROUP BY 语法是 MySQL 特有的 dev.mysql.com/tech-resources/articles/…
    • 您的回答肯定对我有很大帮助 - 但我最终选择了 KandadaBoggu 的解决方案,所以我认为奖励他赏金才公平!谢谢!
    【解决方案4】:

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

    【讨论】:

      猜你喜欢
      • 2014-03-08
      • 2011-08-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-30
      • 2012-11-15
      • 2018-08-27
      • 2020-08-04
      • 2015-11-02
      相关资源
      最近更新 更多