Rails Arel выбирает отдельные столбцы - PullRequest
11 голосов
/ 24 августа 2010

Я столкнулся с небольшим блоком с новыми методами scope (Arel 0.4.0, Rails 3.0.0.rc)

В основном у меня есть:

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

Ответы [ 4 ]

5 голосов
/ 27 августа 2010

Раствор 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 для определения горячих тем.

3 голосов
/ 26 августа 2010

Для этого вам нужно иметь область с 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

Я использовал следующий seed.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 будет представлять другой 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
3 голосов
/ 24 августа 2010

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

Я очень плохо знаком с Арелом, но что-то подобное может работать

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]}
2 голосов
/ 03 апреля 2012

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

Если вы добавите .uniq к Арелю, он добавляет 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

...