Domanda

ho colpito un leggero blocco con i nuovi metodi scope (Arel 0.4.0, Rails 3.0.0.rc)

In pratica ho:

Un modello topics che has_many :comments, e un modello comments (con una colonna topic_id) che belongs_to :topics.

Sto cercando di prendere una raccolta di "Hot Topics", vale a dire gli argomenti che sono stati più recentemente commentate. codice attuale è la seguente:

# models/comment.rb
scope :recent, order("comments.created_at DESC")

# models/topic.rb
scope :hot, joins(:comments) & Comment.recent & limit(5)

Se eseguo Topic.hot.to_sql, la seguente query viene sparato:

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

Questo funziona bene, ma restituisce potenzialmente argomenti duplicati -. Se topic # 3 è stato recentemente commentato più volte, sarebbe tornato più volte

La mia domanda

Come potrei fare per restituire una serie distinta di argomenti, tenendo presente che ho ancora bisogno di accedere al campo comments.created_at, per visualizzare quanto tempo fa l'ultimo post è stato? Immagino qualcosa sulla falsariga di distinct o group_by, ma io non sono troppo sicuro il modo migliore per andare su di esso.

Qualche consiglio / suggerimenti sono molto apprezzate -. Ho aggiunto una taglia 100 rep nella speranza di venire a una soluzione elegante presto

È stato utile?

Soluzione

Soluzione 1

Questo non fa uso di Arel, ma la sintassi 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

Assicurati di indice la colonna created_at e topic_id nella tabella comments.

Soluzione 2

Aggiungi una colonna last_comment_id nel modello Topic. Aggiornare il last_comment_id dopo aver creato un commento. Questo approccio è molto più veloce rispetto all'utilizzo di SQL complesse per determinare l'ultimo commento.

per esempio:

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

Questo è molto più efficiente che in esecuzione di una query SQL complessa per determinare i temi caldi.

Altri suggerimenti

Non è che elegante nella maggior parte delle implementazioni di SQL. Un modo è quello di prima ottenere l'elenco dei cinque più recenti commenti raggruppati per topic_id. Quindi ottenere il comments.created_at da sub selezionando con la clausola IN.

Sono molto di nuovo da Arel, ma qualcosa di simile potrebbe funzionare

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]}

Al fine di raggiungere questo obiettivo è necessario avere un campo di applicazione con un GROUP BY per ottenere l'ultimo commento per ogni argomento. È quindi possibile ordinare questo ambito da created_at per ottenere il più recente commentato argomenti.

Di seguito lavora per me usando 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

ho usato il seguente seeds.rb per generare i dati di test

(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

E Di seguito sono riportati i risultati dei test

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 > 

L'SQL generato per SQLite (riformattato) è estremamente semplice e spero Arel renderebbe SQL diverso per altri motori come questo sarebbe certamente fallire in molti motori di DB come le colonne all'interno topic non sono nel "Gruppo per la lista". Se questo ha fatto un regalo problema allora si potrebbe probabilmente superarla limitando le colonne selezionate a poco 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

Dato che la domanda riguardava Arel, ho pensato che mi piacerebbe aggiungere questo, dal momento che Rails 3.2.1 aggiunge uniq alle QueryMethods:

Se si aggiunge .uniq al Arel aggiunge DISTINCT alla dichiarazione select.

es. Topic.hot.uniq

Funziona anche in campo di applicazione:

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

Quindi presumo che

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

dovrebbe probabilmente anche di lavoro.

http://apidock.com/rails/ActiveRecord/QueryMethods/uniq

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top