Rotaie AREL selezione di colonne distinte
-
30-09-2019 - |
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
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.