2010-08-24 44 views
11

Tôi đã trúng một khối nhẹ với scope phương pháp mới (Arel 0.4.0, Rails 3.0.0.rc)Rails Arel chọn cột riêng biệt

Về cơ bản tôi có:

Một mô hình topics, mà has_many :comments và kiểu comments (có cột topic_id) belongs_to :topics.

Tôi đang cố tìm nạp bộ sưu tập "Chủ đề nóng", tức là các chủ đề được nhận xét gần đây nhất. mã hiện như sau:

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

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

Nếu tôi thực hiện Topic.hot.to_sql, các truy vấn sau đây là bắn:

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

này hoạt động tốt, nhưng nó có khả năng trả về chủ đề trùng lặp - Nếu chủ đề # 3 gần đây đã nhận xét về nhiều lần, nó sẽ được trả lại nhiều lần.

Câu hỏi của tôi

Làm thế nào tôi sẽ đi về trả lại một bộ riêng biệt của chủ đề, mang trong tâm trí mà tôi vẫn cần phải truy cập vào các lĩnh vực comments.created_at, để hiển thị trước các bài trước là bao lâu? Tôi sẽ tưởng tượng một cái gì đó dọc theo các dòng của distinct hoặc group_by, nhưng tôi không quá chắc chắn cách tốt nhất để đi về nó.

Bất kỳ lời khuyên/đề xuất nào được đánh giá cao - tôi đã thêm 100 tiền thưởng đại diện với hy vọng sớm có giải pháp thanh lịch.

Trả lời

5

Giải pháp 1

này không sử dụng Arel, nhưng Rails 2.x cú pháp:

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 

Hãy chắc chắn rằng bạn chỉ số cột created_attopic_id trong bảng comments .

Giải pháp 2

Thêm một cột last_comment_id trong mô hình Topic của bạn. Cập nhật last_comment_id sau khi tạo nhận xét. Cách tiếp cận này nhanh hơn nhiều so với việc sử dụng SQL phức tạp để xác định nhận xét cuối cùng.

ví dụ:

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 

Đây là hiệu quả hơn nhiều so với chạy một truy vấn SQL phức tạp để xác định chủ đề nóng.

+1

Cảm ơn câu trả lời của bạn - đó là giải pháp, nhưng tôi thực sự chỉ tìm kiếm giải pháp sử dụng Rails3/Arel 'scope'! – Jeriko

+0

Cập nhật câu trả lời của tôi, hãy xem. –

+0

Tôi chấp nhận câu trả lời này và trao tiền thưởng cho bạn - Nó không trả lời câu hỏi ban đầu của tôi, nhưng nó thực sự hoạt động tốt hơn theo cách này. Cảm ơn :) – Jeriko

3

Điều này không thanh lịch trong hầu hết các triển khai SQL. Một cách là trước tiên hãy lấy danh sách năm bình luận gần đây nhất được nhóm theo topic_id. Sau đó lấy comments.created_at bằng cách chọn sub với mệnh đề IN.

Tôi rất mới để Arel nhưng một cái gì đó như thế này có thể làm việc

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

Cảm ơn - đây chắc chắn là giải pháp nhưng không hoàn toàn lý tưởng. Các chủ đề được trả về theo thứ tự tùy ý và tôi sẽ không thể truy cập vào thời điểm nhận xét cuối cùng - do đó nỗ lực của tôi tại một bảng tham gia. Không có cách nào để nhóm truy vấn ban đầu của tôi để trả lại kết quả riêng biệt? Tôi cảm thấy như nó gần như ở đó, chỉ là không hoàn toàn. – Jeriko

+0

Ah, sau đó tôi chỉ giải quyết được một nửa vấn đề của bạn. Cách dễ nhất có thể là nhận tất cả nhận xét cho các chủ đề gần đây duy nhất và sau đó chỉ hiển thị các chủ đề mới nhất. SQL để làm điều này thường được ridden với các giải pháp nhà cung cấp cụ thể. Có lẽ hầu hết là một cách ANSI SQL để làm điều này nhưng tôi thực sự nghi ngờ Arel hỗ trợ nó. Hy vọng tôi sai. –

3

Để thực hiện điều này bạn cần phải có một phạm vi với một GROUP BY để có được những nhận xét mới nhất cho từng chủ đề. Sau đó, bạn có thể đặt phạm vi này theo số created_at để nhận được nhận xét gần đây nhất về các chủ đề.

Các công trình sau đây cho tôi sử dụng 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 

tôi đã sử dụng seeds.rb sau đây để tạo ra các dữ liệu thử nghiệm

(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 

Và sau đây là kết quả các thử nghiệm

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 được tạo cho sqlite (được định dạng lại) cực kỳ đơn giản và tôi hy vọng Arel sẽ lại nder SQL khác nhau cho các công cụ khác vì điều này chắc chắn sẽ thất bại trong nhiều công cụ DB vì các cột trong Chủ đề không nằm trong "Nhóm theo danh sách". Nếu điều này đã trình bày một vấn đề thì bạn có thể khắc phục nó bằng cách giới hạn các cột được chọn chỉ để bình luận.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

Tuyệt vời - Tôi chưa có thời gian để kiểm tra, nhưng có vẻ hoàn hảo. Tôi đang sử dụng sqlite trong phát triển, nhưng có khả năng mysql trong sản xuất, vì vậy tôi sẽ phải kiểm tra cách nó dịch - Sẽ trả lời sớm. – Jeriko

+0

Bạn sẽ nhận được sự tạo ra của nhận xét cuối cùng cho một chủ đề trong cách tiếp cận này như thế nào? –

+0

Dường như cú pháp GROUP BY bị giảm là http://dev.mysql.com/tech-resources/articles/debunking-group-by-myths.html –

2

Kể từ khi câu hỏi là về Arel, tôi nghĩ rằng tôi muốn thêm này trong, vì Rails 3.2.1 thêm uniq đến QueryMethods:

Nếu bạn thêm .uniq đến Arel nó thêm DISTINCT để báo cáo kết quả select.

ví dụ: Topic.hot.uniq

Cũng hoạt động trong phạm vi:

ví dụ: scope :hot, joins(:comments).order("comments.created_at DESC").limit(5).uniq

Vì vậy, tôi sẽ giả định rằng

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

cũng nên có lẽ làm việc.

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

Các vấn đề liên quan