2010-08-24 2 views
11

я ударил небольшой блок с новыми scope методами (Arel 0.4.0, Rails 3.0.0.rc)Rails Арел выбора различных столбцов

В принципе у меня есть:

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-страничную награду в надежде скоро прийти к элегантному решению.

ответ

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_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 запрос, чтобы определить горячие темы.

+1

Спасибо за ваш ответ - это решение, но я действительно ищу только тот, который использует Rails3/Arel 'scope'! – Jeriko

+0

Обновлен мой ответ, посмотрите. –

+0

Я принимаю этот ответ и награждаю вас щедростью. Это не отвечает на мой первоначальный вопрос, но в любом случае он работает лучше. Спасибо :) – Jeriko

3

Это не так элегантно в большинстве реализаций SQL. Один из способов - сначала получить список пяти последних комментариев, сгруппированных по topic_id. Затем получите комментарии comments.created_at путем выбора sub с предложением IN.

Я очень новый для 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]} 
+0

Спасибо - это, безусловно, решение, но это не совсем идеально. Темы возвращаются в произвольном порядке, и я не смогу получить доступ ко времени последнего комментария, поэтому моя попытка соединения таблицы. Невозможно сгруппировать мой исходный запрос, чтобы возвращать отличные результаты? Я чувствую, что он почти там, просто не совсем. – Jeriko

+0

А, тогда я решил не больше половины вашей проблемы. Самый простой способ - получить все комментарии к уникальным недавним темам, а затем показать только последние. SQL, чтобы сделать это, часто сопровождается решениями для конкретных поставщиков. Скорее всего, это способ ANSI SQL, но я действительно сомневаюсь, что он поддерживает его. Надеюсь, я ошибаюсь. –

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 > 

SQL, созданный для sqlite (переформатированный), чрезвычайно прост, и я надеюсь, что Arel re re другой SQL для других двигателей, поскольку это, конечно же, не удастся во многих механизмах БД, поскольку столбцы в теме не входят в «Групповой список». Если это создало проблему, вы могли бы, вероятно, преодолеть ее, ограничив выделенные столбцы просто комментариями.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 
+0

Awesome - Я еще не успел его протестировать, но он выглядит идеально. Я использую sqlite в разработке, но потенциально mysql в производстве, поэтому мне нужно будет проверить, как он переводится - скоро ответит. – Jeriko

+0

Как вы получите созданный_данец последнего комментария для темы в этом подходе? –

+0

Похоже, что сокращенный синтаксис GROUP BY является специфичным для MySQL: http://dev.mysql.com/tech-resources/articles/debunking-group-by-myths.html –

2

Поскольку речь шла о Arel, я думал, что я хотел бы добавить это, так как Rails 3.2.1 добавляет uniq к QueryMethods:

Если добавить .uniq к Arel добавляет DISTINCTselect в заявлении.

например. 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

Смежные вопросы