Rails Arel اختيار أعمدة مميزة
-
30-09-2019 - |
سؤال
لقد ضربت كتلة طفيفة مع الجديد 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 مكافأة مندوب على أمل الوصول إلى حل أنيق قريبًا.
المحلول
الحل 1
هذا لا يستخدم Arel ، ولكن بناء الجملة 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 معقد لتحديد الموضوعات الساخنة.
نصائح أخرى
هذا ليس أنيقًا في معظم تطبيقات SQL. تتمثل إحدى الطرق في الحصول على قائمة التعليقات الخمسة الأخيرة التي تم تجميعها بواسطة Topic_ID. ثم احصل على التعليقات.
أنا جديد جدًا على أريل ولكن شيء من هذا القبيل يمكن أن يعمل
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]}
من أجل تحقيق ذلك ، تحتاج إلى الحصول على نطاق مع أ 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
لقد استخدمت البذور التالية. 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 مختلفًا للمحركات الأخرى لأن هذا سيفشل بالتأكيد في العديد من محركات DB لأن الأعمدة الموجودة في الموضوع ليست في "المجموعة حسب القائمة". إذا كان هذا يمثل مشكلة ، فربما يمكنك التغلب عليها من خلال الحد من الأعمدة المحددة على التعليقات فقط. 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
نظرًا لأن السؤال كان حول Arel ، اعتقدت أنني سأضيف هذا ، منذ أن أضيف 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
يجب أن تعمل أيضًا.