Вложенные комментарии с нуля

Скажем, у меня есть модель комментария:

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).

Ответ 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 %>

Хотя я не уверен, что это был бы самый обычный способ с точки зрения затрат.