11

scope新しいメソッド(Arel 0.4.0、Rails 3.0.0.rc)でわずかなブロックにぶつかりました

基本的に私は持っています:

topicsモデル、which has_many :comments、およびcommentsモデル(topic_id列付き)which 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レップの報奨金を追加しました。

4

4 に答える 4

5

解決策 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_attopic_id列に必ずインデックスを付けてください。comments

解決策 2

last_comment_idモデルに列を追加しますTopiclast_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 クエリを実行して最新のトピックを特定するよりもはるかに効率的です。

于 2010-08-27T05:14:10.020 に答える
3

これは、ほとんどの SQL 実装ではそれほど洗練されていません。1 つの方法は、最初に topic_id でグループ化された最新の 5 つのコメントのリストを取得することです。次に、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]}
于 2010-08-24T09:55:31.957 に答える
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 は非常に単純です。Topic 内の列が "Group by list" にないため、多くの DB エンジンでは確実に失敗するため、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
于 2010-08-26T10:57:05.630 に答える
2

質問は Arel に関するものだったので、Rails 3.2.1 がuniqQueryMethods に追加されたので、これを追加すると思いました:

.uniqArel に追加する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を参照してください

于 2012-04-03T16:09:45.183 に答える