【问题标题】:What is the right approach to using recursive function in a rails view?在 Rails 视图中使用递归函数的正确方法是什么?
【发布时间】:2019-04-25 11:46:39
【问题描述】:

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

评论模型使用自引用 id。

我有一个名为 comment 的局部视图,它呈现单个评论,我正在尝试使用递归函数逐个绘制每个评论。

查看

<% 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 视图中呈现树结构?

【问题讨论】:

    标签: ruby-on-rails


    【解决方案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
    

    为每条评论运行单独的查询将对大型线程产生过多的负载。

    【讨论】:

      【解决方案2】:

      在我看来,这是在 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 %>
      

      【讨论】:

      • 这个解决方案与标记有类似的问题,因为它只处理一个深度级别。它不会渲染children的children和children的children等
      • 这根本不是真的。每个部分为它的每个孩子呈现一个新的部分。我现在正在生产中使用此代码。这很简单。这是一个递归解决方案。只要有子 cmets,_comment.html.erb 部分就会呈现 自身。它不会在一次渲染后停止。它继续递归地渲染自身,直到达到最大深度或堆栈溢出。因此,我提供了硬编码限制的解决方案。我现在仔细检查了我的代码。这是 100% 的工作。所以,我不知道你在说什么@Jamesla。
      • 当您在正确的轨道上时,您的 children 方法只是一个简单的坏主意。而是创建一个关联,以便您可以加入孩子并避免 n+1 查询。
      • 是的,我同意你的看法。虽然我的解决方案有效,但它会导致 N+1 查询。但是对于我自己的爱好项目?是的,这是一个简单快速的解决方案。我只是想澄清一下,我的解决方案是一个有效的 Rails 解决方案,可以产生预期的结果,但是我从这个线程中得到了关于我的解决方案没有解决的 N+1 问题的提醒。因此,感谢您为后代澄清,因为未来的人们可能会错误地使用我的。我应该删除我的答案吗?
      • 你说得对,我第一眼没看懂。这很好用!
      【解决方案3】:

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

      您应该首先设置适当的关联,这样您就可以在评论实例上调用 #children 来获取嵌套的 cmets,而不是执行 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>
      

      您实际上并不需要辅助方法和额外的复杂性。

      【讨论】:

        【解决方案4】:

        我相信处理这个问题的最好方法是遍历你的 children 集合,并在你的视图中为每个集合渲染一个部分:

        <% 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 的相同部分。

        【讨论】:

        • 此解决方案仅在有 2 个深度级别(父级和子级)时才有效,但是如上所述,深度的数量是无限的。即有人可以发表评论,有人可以回复该评论,有人可以回复该评论,有人可以回复该评论等(通过评论上的自引用ID)
        • 你能不能像我一样在辅助方法中创建一个子元素的集合并使用它来迭代?
        • 不,因为你需要渲染孩子的孩子,然后是这些孩子的孩子等等,因为你不知道有多少深度需要动态完成跨度>
        猜你喜欢
        • 2014-09-20
        • 1970-01-01
        • 2010-11-19
        • 1970-01-01
        • 1970-01-01
        • 2010-11-10
        • 1970-01-01
        • 2018-05-30
        • 1970-01-01
        相关资源
        最近更新 更多