Вложенные комментарии с нуля
Скажем, у меня есть модель комментария:
class Comment < ActiveRecord::Base
has_many :replies, class: "Comment", foreign_key: "reply_id"
end
Я могу показать ответы экземпляра комментария в виде так:
comment.replies do |reply|
reply.content
end
Однако, как мне пройти через ответы ответа? И его ответ? И его ответ ad infitum? Я чувствую, что нам нужно сделать многомерный массив ответов через метод класса, а затем пропустить этот массив в представлении.
Я не хочу использовать драгоценный камень, я хочу узнать
Ответы
Ответ 1
Кажется, что у вас есть один короткий шаг от того, что вы хотите. Вам просто нужно использовать рекурсию для вызова одного и того же кода для каждого ответа, когда вы вызываете исходные комментарии. Например.
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: @comments %>
</div>
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: comment.replies %>
</div>
NB: это не самый эффективный способ делать вещи. Каждый раз, когда вы вызываете активную запись comment.replies, выполняется другой запрос базы данных. Там определенно есть возможность для улучшения, но основная идея в любом случае.
Ответ 2
Будет ли использование вложенного набора по-прежнему считаться "с нуля"?
Краткое описание вложенного набора - это стратегия, ориентированная на конкретную базу данных запросов иерархии, путем хранения/запроса запросов обхода трассировки до и после заказа.
Изображение стоит тысячи слов (см. также википедия страница на вложенных наборах).
Есть множество вложенных наборов gems, и я могу лично говорить за качество Awesome Nested Set и Ancestry
Затем Awesome Nested Set (я знаю по опыту, предположительно, Ancestry тоже) предоставляет помощникам сделать один запрос, чтобы вытащить все записи под деревом и выполнить итерацию по дереву в отсортированном порядке глубины-первого порядка, проходящем на уровне пока вы идете.
Код представления для Awesome Nested Set будет выглядеть примерно так:
<% Comment.each_with_level(@post.comments.self_and_descendants) do |comment, level| %>
<div style="margin-left: <%= level * 50 %>px">
<%= comment.body %>
<%# etc %>
</div>
<% end %>
Я только что сделал это из-за смутных воспоминаний, и это было какое-то время, поэтому это может быть "упражнение для читателя"
Ответ 3
Мой подход заключается в том, чтобы сделать это максимально эффективным.
Сначала давайте рассмотрим, как это сделать:
- DRY.
- Наименьшее количество запросов для получения комментариев.
Размышляя об этом, я обнаружил, что большинство людей обращаются к первому, но не к второму. Так что начнем с простого.
мы должны иметь частичные комментарии, ссылаясь на ответ jeanaux
мы можем использовать его подход для отображения комментариев и обновим его позже в ответ
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: @comments %>
</div>
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: comment.replies %>
</div>
Мы должны теперь получить эти комментарии в одном запросе, если это возможно, чтобы мы могли просто сделать это в контроллере. чтобы иметь возможность сделать это, все комментарии и ответы должны иметь commentable_id (и введите if polymorphic), чтобы при запросе мы могли получить все комментарии, а затем группировать их так, как мы хотим.
Итак, если у нас есть сообщение, и мы хотим получить все его комментарии, мы скажем в контроллере.
@comments = @post.comments.group_by {| c | c.reply_id}
этим мы имеем комментарии в одном запросе, обработанном для отображения непосредственно
Теперь мы можем сделать это, чтобы отобразить их вместо того, что мы ранее делали
Все комментарии, которые не являются ответами, теперь находятся в @comments [nil], так как они не ответили
(NB: Мне не нравится @comments [nil], если у кого-либо есть какие-либо другие предложения, пожалуйста, прокомментируйте или отредактируйте)
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: @comments[nil] %>
</div>
Все ответы для каждого комментария будут находиться в папке с идентификатором родительского комментария
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: @comments[comment.id] %>
</div>
Завершить:
- Мы добавили object_id в модель комментария, чтобы иметь возможность получить
их (если они еще не существуют)
- Мы добавили группировку reply_id в
получить комментарии с одним запросом и обработать их для представления.
- Мы добавили часть, которая рекурсивно отображает комментарии (как предложенный jeanaux).
Ответ 4
Похоже, вам нужна ассоциативная ассоциация. Проверьте следующие railscast: http://railscasts.com/episodes/163-self-referential-association
Ответ 5
Мы сделали это:
![enter image description here]()
Мы использовали ancestry
gem для создания иерархического набора данных, а затем выведено с частичным выводом ordered list
:
#app/views/categories/index.html.erb
<% # collection = ancestry object %>
<%= render partial: "category", locals: { collection: collection } %>
#app/views/categories/_category.html.erb
<ol class="categories">
<% collection.arrange.each do |category, sub_item| %>
<li>
<!-- Category -->
<div class="category">
<%= link_to category.title, edit_admin_category_path(category) %>
<%= link_to "+", admin_category_new_path(category), title: "New Categorgy", data: {placement: "bottom"} %>
<% if category.prime? %>
<%= link_to "", admin_category_path(category), title: "Delete", data: {placement: "bottom", confirm: "Really?"}, method: :delete, class: "icon ion-ios7-close-outline" %>
<% end %>
<!-- Page -->
<%= link_to "", new_admin_category_page_path(category), title: "New Page", data: {placement: "bottom"}, class: "icon ion-compose" %>
</div>
<!-- Pages -->
<%= render partial: "pages", locals: { id: category.name } %>
<!-- Children -->
<% if category.has_children? %>
<%= render partial: "category", locals: { collection: category.children } %>
<% end %>
</li>
<% end %>
</ol>
Мы также создали вложенное выпадающее меню:
![enter image description here]()
#app/helpers/application_helper.rb
def nested_dropdown(items)
result = []
items.map do |item, sub_items|
result << [('- ' * item.depth) + item.name, item.id]
result += nested_dropdown(sub_items) unless sub_items.blank?
end
result
end
Ответ 6
Это можно решить с помощью рекурсии или со специальной структурой данных. Рекурсия проще реализовать, тогда как структура данных, подобная той, что используется камнем nested_set
, более эффективна.
Рекурсия
Сначала пример того, как он работает в чистом Ruby.
class Comment < Struct.new(:content, :replies);
def print_nested(level = 0)
puts "#{' ' * level}#{content}" # handle current comment
if replies
replies.each do |reply|
# here is the list of all nested replies generated, do not care
# about how deep the subtree is, cause recursion...
reply.print_nested(level + 1)
end
end
end
end
Пример
comments = [ Comment.new(:c_1, [ Comment.new(:c_1a) ]),
Comment.new(:c_2, [ Comment.new(:c_2a),
Comment.new(:c_2b, [ Comment.new(:c_2bi),
Comment.new(:c_2bii) ]),
Comment.new(:c_2c) ]),
Comment.new(:c_3),
Comment.new(:c_4) ]
comments.each(&:print_nested)
# Output
#
# c_1
# c_1a
# c_2
# c_2a
# c_2b
# c_2bi
# c_2bii
# c_2c
# c_3
# c_4
И теперь, когда рекурсивные вызовы Rails рассматривают частичные файлы:
# in your comment show view
<%= render :partial => 'nested_comment', :collection => @comment.replies %>
# recursion in a comments/_nested_comment.html.erb partial
<%= nested_comment.content %>
<%= render :partial => 'nested_comment', :collection => nested_comment.replies %>
Вложенный набор
Настройте структуру своей базы данных, см. документы: http://rubydoc.info/gems/nested_set/1.7.1/frames Это добавит что-то вроде следующего (непроверенного) к вашему приложению.
# in model
acts_as_nested_set
# in controller
def index
@comment = Comment.root # `root` is provided by the gem
end
# in helper
module NestedSetHelper
def root_node(node, &block)
content_tag(:li, :id => "node_#{node.id}") do
node_tag(node) +
with_output_buffer(&block)
end
end
def render_tree(hash, options = {}, &block)
if hash.present?
content_tag :ul, options do
hash.each do |node, child|
block.call node, render_tree(child, &block)
end
end
end
end
def node_tag(node)
content_tag(:div, node.content)
end
end
# in index view
<ul>
<%= render 'tree', :root => @comment %>
</ul>
# in _tree view
<%= root_node(root) do %>
<%= render_tree root.descendants.arrange do |node, child| %>
<%= content_tag :li, :id => "node_#{node.id}" do %>
<%= node_tag(node) %>
<%= child %>
<% end %>
<% end %>
<% end %>
Этот код написан из старого приложения Rails 3.0, слегка измененного и непроверенного. Поэтому он, вероятно, не будет работать из коробки, но должен проиллюстрировать эту идею.
Ответ 7
Это будет мой подход:
- У меня есть модель комментариев и модель ответа.
- Комментарий has_many association with Reply
- Ответ имеет принадлежность к ассоциации с комментарием
-
Ответ имеет собственный референтный HABTM
class Reply < ActiveRecord::Base
belongs_to :comment
has_and_belongs_to_many :sub_replies,
class_name: 'Reply',
join_table: :replies_sub_replies,
foreign_key: :reply_id,
association_foreign_key: :sub_reply_id
def all_replies(reply = self,all_replies = [])
sub_replies = reply.sub_replies
all_replies << sub_replies
return if sub_replies.count == 0
sub_replies.each do |sr|
if sr.sub_replies.count > 0
all_replies(sr,all_replies)
end
end
return all_replies
end
end
Теперь, чтобы получить ответ от комментария и т.д.:
- Получение всех ответов от комментария: @comment.replies
- Получение комментария от любого ответа: @reply.comment
- Получение промежуточного уровня ответов из ответа: @reply.sub_replies
- Получение ответов на все уровни ответов: @reply.all_replies
Ответ 8
У меня был отличный, как правило, плохой опыт работы с различными драгоценными камнями иерархии, доступными для ActiveRecord. Как правило, вы не хотите делать это самостоятельно, так как ваши запросы окажутся очень неэффективными.
Драгоценный камень Ancestry был в порядке, но мне пришлось отойти от него, потому что "дети" - это область действия и НЕ ассоциация. Это означает, что вы НЕ МОЖЕТЕ использовать вложенные атрибуты, потому что вложенные атрибуты работают только с ассоциациями, а не с областями. Это может быть или не быть проблемой в зависимости от того, что вы делаете, например, заказывать или обновлять братьев и сестер через родителя или обновлять целые поддеревья/графики за одну операцию.
Самый эффективный драгоценный камень ActiveRecord для этого - это Closure Tree gem, и у меня были хорошие результаты с ним, с предостережением о том, что splatting/mutating whole поддеревья были дьявольскими из-за того, как работает ActiveRecord. Если вам не нужно вычислять вещи по дереву при выполнении обновлений, это путь.
С тех пор я отошел от ActiveRecord к Sequel и имеет рекурсивную поддержку общих табличных выражений (RCTE), которая используется встроенным плагином дерева. Дерево RCTE так же быстро, как теоретически возможно обновить (просто измените один parent_id как в наивной реализации), а запрос также обычно на порядок быстрее, чем другие подходы из-за используемой функции SQL RCTE. Это также самый эффективный с точки зрения пространства подход, поскольку поддерживается только parent_id. Я не знаю никаких решений ActiveRecord, которые поддерживают деревья RCTE, потому что ActiveRecord не охватывает почти столько же спектра SQL, что и Sequel.
Если вы не привязаны к ActiveRecord, то Sequel и Postgres - это грозная комбинация IMO. Вы обнаружите недостатки в AR, когда ваши запросы становятся настолько сложными. Всегда есть боль, переносящаяся на другую ORM, поскольку ее подход не используется, но я смог выразить запросы, которые я не мог сделать с ActiveRecord или ARel (хотя они были довольно простыми) и обычно улучшал запрос производительность по всем направлениям в 10-20 раз выше того, что я получал с помощью ActiveRecord. В моем случае с сохранением деревьев данных в сотни раз быстрее. Это означает, что инфраструктура сервера в десятки и сотни раз меньше, чем требуется для той же нагрузки. Подумайте об этом.
Ответ 9
Вы должны собирать ответы ответов в каждой итерации ответа.
<% comment.replies do |reply| %>
<%= reply.content %>
<% reply_replies = Post.where("reply_id = #{reply.id}").all %>
<% reply_replies .each do |p| %>
<%= p.post %>
<% end
<% end %>
Хотя я не уверен, что это был бы самый обычный способ с точки зрения затрат.