嵌套评论从头开始

假设我有一个评论模型:

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

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

 comment.replies do |reply| reply.content end 

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

我不想使用gem,我想学习

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

  
<%= render partial: "comment", collection: @comments %>

<%= comment.content %>

<%= render partial: "comment", collection: comment.replies %>

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

使用嵌套集仍然算作’从头开始’?

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

一张图片胜过千言万语 (另请参阅嵌套集上的维基百科页面 )。

有一堆嵌套的gem ,我可以亲自代表令人敬畏的嵌套和祖先的质量

然后,Awesome Nested Set(我从经验中知道,也可能是Ancestry)提供帮助程序来执行单个查询以提取树下的所有记录,并按照排序的深度优先顺序遍历树,在您去的时候传入关卡。

Awesome Nested Set的视图代码如下:

 <% Comment.each_with_level(@post.comments.self_and_descendants) do |comment, level| %> 
<%= comment.body %> <%# etc %>
<% end %>

我只是从模糊的记忆中做出来,而且已经有一段时间了,所以这就是“为读者做练习”的地方

我的方法是尽可能高效地完成这项工作。 首先让我们解决如何做到这一点:

  1. 干解决方案。
  2. 至少检索注释的查询数。

考虑到这一点,我发现大多数人都会解决第一个问题而不是第二个问题。所以让我们从容易的问题开始。 我们必须部分参与评论,以便参考jeanaux的答案

我们可以使用他的方法来显示评论,并在稍后的答案中更新

  
<%= render partial: "comment", collection: @comments %>

<%= comment.content %>

<%= render partial: "comment", collection: comment.replies %>

我们现在必须在一个查询中检索这些注释,以便我们可以在控制器中执行此操作。 为了能够做到这一点,所有的评论和回复应该有一个commentable_id(并且如果是多态的则输入),这样当我们查询时我们可以获得所有评论,然后按照我们想要的方式对它们进行分组。

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

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

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

  
<%= render partial: "comment", collection: @comments[nil] %>

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

  

<%= comment.content %>

<%= render partial: "comment", collection: @comments[comment.id] %>

总结:

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

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

我们这样做了:

在此处输入图像描述

我们使用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 
    <% collection.arrange.each do |category, sub_item| %>
  1. <%= 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 %> <%= link_to "", new_admin_category_page_path(category), title: "New Page", data: {placement: "bottom"}, class: "icon ion-compose" %>
    <%= render partial: "pages", locals: { id: category.name } %> <% if category.has_children? %> <%= render partial: "category", locals: { collection: category.children } %> <% end %>
  2. <% end %>

我们还制作了一个嵌套下拉列表:

在此处输入图像描述

 #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 

这可以通过重新生成或使用特殊数据结构来解决。 递归更容易实现,而像nested_set gem使用的数据结构更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 
    <%= render 'tree', :root => @comment %>
# 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应用程序,略有变化且未经测试。 因此它可能不会开箱即用,但应该说明这个想法。

这将是我的方法:

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

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

Ancestry的gem很好,但是我不得不放弃它,因为“孩子”是一个范围,而不是一个关联。 这意味着您不能使用嵌套属性,因为嵌套属性仅适用于关联,而不适用于范围。 根据您正在做的事情,这可能是也可能不是问题,例如通过父级排序或更新兄弟姐妹,或者在单个操作中更新整个子树/图表。

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

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

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

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

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

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