嵌套评论从头开始

时间:2014-03-25 13:24:00

标签: ruby-on-rails ruby

假设我有一个评论模型:

class Comment < ActiveRecord::Base
    has_many :replies, class: "Comment", foreign_key: "reply_id"
end

我可以在视图中显示评论实例的回复:

comment.replies do |reply|
   reply.content
end

但是,如何循环回复回复?它的回复?它的回复广告?我觉得我们需要通过类方法创建一个多维的回复数组,然后在视图中循环遍历这个数组。

我不想使用宝石,我想学习

9 个答案:

答案 0 :(得分:8)

看起来你所拥有的只是距你想要的一步之遥。您只需要使用递归为每个回复调用相同的代码,因为您正在调用原始注释。 E.g。

<!-- 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>

注意:这不是最有效的做事方式。每次调用comment.replies时,活动记录都会运行另一个数据库查询。肯定有改进的余地,但无论如何这都是基本的想法。

答案 1 :(得分:3)

使用嵌套集仍然会从头开始算作&#39;?

嵌套集的简短描述是一种特定于数据库的策略,通过存储/查询订单前和订单后的树遍历计数来查询层次结构。

A picture is worth a thousand words(另请参阅the wikipedia page on nested sets)。

There are a bunch of nested set gems,我个人可以代表Awesome Nested SetAncestry

的质量发言

然后,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 %>

我只是从模糊的回忆中做出来,并且已经有一段时间了,所以这就是它可以成为读者的一种练习&#34;

答案 2 :(得分:3)

我的方法是尽可能高效地完成这项工作。 首先介绍如何执行此操作:

  1. DRY解决方案。
  2. 检索评论的最少查询数。
  3. 考虑到这一点,我发现大多数人都会解决第一个问题,而不是第二个问题。所以让我们从容易的问题开始。 我们必须部分参与评论,以便引用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(并且如果是多态的则输入),这样当我们查询时我们可以获得所有评论,然后按照我们想要的方式对它们进行分组。

    因此,如果我们有一个帖子,例如我们希望获得所有评论,我们将在控制器中说明。 @comments = @ post.comments.group_by {| c | c.reply_id}

    通过这个我们在一个查询中有注释处理直接显示 现在我们可以这样做来显示它们而不是我们以前做过的事情

    所有未回复的评论现在都在@comments [nil]中,因为它们没有reply_id (注意:我不喜欢@comments [nil]如果有人有任何其他建议请评论或编辑)

    <!-- view -->
    <div id="comments">
      <%= render partial: "comment", collection: @comments[nil] %>
    </div>
    

    每条评论的所有回复都将在父评论ID

    下的内容中
    <!-- _comment partial -->
    <div class="comment">
      <p><%= comment.content %></p>
      <%= render partial: "comment", collection: @comments[comment.id] %>
    </div> 
    

    总结:

    1. 我们在评论模型中添加了一个object_id以便能够检索 他们(如果还没有)
    2. 我们通过reply_id添加了分组 使用一个查询检索注释并为视图处理它们。
    3. 我们添加了一个递归显示注释的部分(如     由jeanaux提出。

答案 3 :(得分:2)

好像你需要一个自我指涉的联想。查看以下railscast:http://railscasts.com/episodes/163-self-referential-association

答案 4 :(得分:2)

我们已经这样做了:

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

答案 5 :(得分:2)

这可以通过重新使用或使用特殊的数据结构来解决。递归更容易实现,而像nested_set gem使用的数据结构更具性能。

递归

首先是一个如何在纯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查看partials:

# 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应用程序,略有变化且未经测试。因此它可能不会开箱即用,但应该说明这个想法。

答案 6 :(得分:1)

这将是我的方法:

  • 我有一个评论模型和一个回复模型。
  • 评论has_many与回复
  • 的关联
  • 回复已将belongs_to与评论相关联
  • 回复有自我参照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

答案 7 :(得分:0)

我对ActiveRecord可用的不同层次结构宝石有各种不同的体验。通常,您不希望自己这样做,因为您的查询最终效率非常低。

Ancestry的宝石还可以,但我不得不离开它,因为孩子&#39;是范围而不是关联。这意味着您不能使用嵌套属性,因为嵌套属性仅适用于关联,而不适用于范围。根据您正在做的事情,这可能是也可能不是问题,例如通过父级排序或更新兄弟姐妹或在单个操作中更新整个子树/图表。

最有效的ActiveRecord宝石就是Closure Tree宝石,我对它有很好的效果,但由于ActiveRecord的工作方式,整个子树的喷溅/变异都是恶魔般的。如果您在进行更新时不需要在树上计算事物,那么就可以了。

我已经从ActiveRecord转移到Sequel,它具有递归的公用表表达式(RCTE)支持,它由内置的树插件使用。 RCTE树在理论上可以快速更新(只是在一个简单的实现中修改单个parent_id),并且由于它使用的SQL RCTE特性,查询通常也比其他方法快几个数量级。它也是最节省空间的方法,因为只需要parent_id来维护。我不知道任何支持RCTE树的ActiveRecord解决方案,因为ActiveRecord几乎没有覆盖续集所做的SQL频谱。

如果您没有与ActiveRecord结合,那么Sequel和Postgres是一个强大的组合IMO。当您的查询变得如此复杂时,您将发现AR中的缺陷。总是有一种痛苦转移到另一个ORM,因为它不是开箱即用的股票轨道方法,但我已经能够表达我无法用ActiveRecord或ARel做的查询(即使它们非常简单),并且通常与我使用ActiveRecord获得的相比,全面提高了查询性能10-20倍。在我的用例中,维护数据树的速度要快几百倍。这意味着对于相同的负载,我需要的服务器基础设施要少几十到几百倍。想一想。

答案 8 :(得分:-1)

您将在每个回复迭代中收集回复的回复。

<% comment.replies do |reply| %>
   <%= reply.content %>
   <% reply_replies = Post.where("reply_id = #{reply.id}").all %>
   <% reply_replies .each do |p| %>
     <%= p.post %>
   <% end
<% end %>

虽然我不确定它是否是成本最常规的方式。