在Rails视图中使用递归函数的正确方法是什么?

时间:2019-04-25 11:46:39

标签: ruby-on-rails

我正在构建类似于reddit的嵌套评论系统,其中评论嵌套在另一个评论下,并且深度几乎不受限制。

评论模型使用一个自引用ID。

我有一个称为注释的局部视图,该视图呈现单个注释,并且我试图使用递归函数逐个绘制每个注释。

视图

<% comments.where(parent_id: nil).each do |parent| %>
  <!-- render root node -->
  <%= render partial: "comment", locals: { comment: parent } %>
  <!-- recursively render child nodes -->
  <%= render_children(parent.id) %>
<% end %>

助手

def render_children(id)
  Comment.where(parent_id: id).each do |comment|
    render partial: "comment", locals: { comment: comment }
    render_children(comment.id)
  end 
end

这是行不通的,因为助手不能多次调用render。我也尝试过在我的视图中定义一个函数,但似乎也不喜欢。

我想知道我是否以错误的方式解决了这个问题。

使用递归函数使我可以在rails视图中渲染树结构的正确方法是什么?

4 个答案:

答案 0 :(得分:1)

Helper可以渲染多个,但是必须组合结果字符串并仅返回一个:

def render_children(id)
  children = Comment.where(parent_id: id).to_a
  safe_join(
    children.map{|comment|
      safe_join([
        render(partial: "comment", locals: { comment: comment }),
        render_children(comment.id)
      ])
    }
  )
end

每个注释运行单独的查询会对大型线程产生过多的负载。

答案 1 :(得分:0)

我认为,这是在Rails中递归呈现组件的最清晰方法:

在部分_comment.html.erb

<%= comment.data %>
<% comment.children.each do |child| %>
  <ul class="child-thread">
    <%= render partial: 'comments/comment', locals: { comment: child } %>
  </ul>
<% end %>

您的Comment模型应具有以下方法:

class Comment < ApplicationRecord
  has_one :parent, :foreign_key => :parent_id
  def children 
    Comment.where(parent_id: self.id)
  end
end

因此,注释的parent_id可以为NULL(在根级注释的情况下),也可以为另一个comment.id。您可以通过在行中传递局部变量来轻松地在递归级别上设置硬锁:

<%= comment.data %>
<% if count < 5 %>
  <% comment.children.each do |child| %>
    <ul class="child-thread">
      <%= render partial: 'comments/comment', 
          locals: { comment: child, count: count + 1 } %>
    </ul>
  <% end %>
<% end %>

答案 2 :(得分:0)

此代码是N+1 query的出色示例,它将破坏应用程序的性能。每次迭代都调用Comment.where(parent_id: id),这将创建一个附加的数据库查询。

您应该首先建立适当的关联,这样您就可以只在评论实例上调用#children来获取嵌套评论,而不用执行Comment.where(parent_id: id)

class Comment
  belongs_to :parent, class_name: 'Comment', optional: true
  has_many :children, class_name: 'Comment', foreign_key: :parent_id
end

这将使您可以使用.includes.eager_load在同一查询中获取子项:

<% render partial: 'comment', collection: Comment.where(parent_id: nil).includes(:children) %>

但是,这只能深入一层。 Rails并不真正支持递归关联加载,但是您可以伪造它:

class Comment < ApplicationRecord
  belongs_to :parent, class_name: 'Comment', optional: true
  has_many :children, class_name: 'Comment', foreign_key: :parent_id

  def self.deep_includes(levels = 5)
    hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc)  }
    keys = Array.new(levels, :children)
    keys.inject(hash) {|h, k| h[k] }[:children] = :children  
    self.includes(hash)
  end
end

这确实可以做到:

Comment.includes({:children=>{:children=>{:children=>{:children=>{:children=>{:children=>:children}}}}}})

哪个会产生一个巨大的SQL查询,该查询会连接到每个级别。

<% render partial: 'comment', collection: Comment.where(parent_id: nil).deep_includes %>

通过这种方法,使递归部分实际上比您想象的要容易得多:

# app/views/comments/_comment.html.erb
<div class="comment">
   <p><%= comment.data %></p>
   <% if comment.children.any? %>
   <div class="children">
     <%= render partial: "comment", collection: comment.children %>
   </div>
   <% end %>
</div>

您真的不需要辅助方法和额外的复杂性。

答案 3 :(得分:-1)

我相信处理此问题的最佳方法是遍历子级集合,并为视图中的每个子项渲染一部分:

<% comments.where(parent_id: nil).each do |parent| %>
  <!-- render root node -->
  <%= render partial: "comment", locals: { comment: parent } %>
  <% parent.children.each do |child| %>
    <%= render partial: "comment", locals: { comment: child } %>
  <% end %>
<% end %>

请注意,这假定您的父对象有许多children(如果不是这样,则用任何相关的关系替换),并且您要为父对象和对象都渲染一个称为comment的相同部分。孩子们。