我正在构建类似于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视图中渲染树结构的正确方法是什么?
答案 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
的相同部分。孩子们。