【问题标题】:How to render all records from a nested set into a real html tree如何将嵌套集中的所有记录呈现到真正的 html 树中
【发布时间】:2010-11-25 05:37:54
【问题描述】:

我在我的 Rails 项目中使用awesome_nested_set 插件。我有两个看起来像这样的模型(简化):

class Customer < ActiveRecord::Base
  has_many :categories
end

class Category < ActiveRecord::Base
  belongs_to :customer

  # Columns in the categories table: lft, rgt and parent_id
  acts_as_nested_set :scope => :customer_id

  validates_presence_of :name
  # Further validations...
end

数据库中的树按预期构建。 parent_idlftrgt 的所有值都是正确的。树有多个根节点(awesome_nested_set 中当然允许)。

现在,我想以正确排序的树状结构呈现给定客户的所有类别:例如嵌套的&lt;ul&gt; 标签。这不会太难,但我需要它高效(sql 查询越少越好)。

更新: 发现无需进一步的 SQL 查询即可计算树中任何给定节点的子节点数:number_of_children = (node.rgt - node.lft - 1)/2。这并不能解决问题,但可能会有所帮助。

【问题讨论】:

    标签: sql ruby-on-rails ruby activerecord nested-sets


    【解决方案1】:

    如果嵌套集具有开箱即用的更好功能,那就太好了。

    您发现的技巧是从一个平面集构建树:

    • 从一组按 lft 排序的所有节点开始
    • 第一个节点是根节点添加它作为树的根节点移动到下一个节点
    • 如果它是前一个节点的子节点(lft 在 prev.lft 和 prev.rht 之间),则向树中添加一个子节点并向前移动一个节点
    • 否则将树向上移动一级并重复测试

    见下文:

    def tree_from_set(set) #set must be in order
      buf = START_TAG(set[0])
      stack = []
      stack.push set[0]
      set[1..-1].each do |node|
        if stack.last.lft < node.lft < stack.last.rgt
          if node.leaf? #(node.rgt - node.lft == 1)
            buf << NODE_TAG(node)
          else
            buf << START_TAG(node)
            stack.push(node)
          end
        else#
          buf << END_TAG
          stack.pop
          retry
        end
      end
      buf <<END_TAG
    end
    
    def START_TAG(node) #for example
      "<li><p>#{node.name}</p><ul>"
    end
    
    def NODE_TAG(node)
      "<li><p>#{node.name}</p></li>"
    end
    
    def END_TAG
      "</li></ul>"
    end 
    

    【讨论】:

    • 这行得通。你对 awesome_nested_set 也是正确的。我不禁想知道为什么这不是首先内置在插件中的。谢谢!
    • 忘了提及:关于您的解决方案的要点是,它只需要一个 SQL 查询!
    【解决方案2】:

    我最近回答了一个similar question for php(嵌套集 == 修改前序树遍历模型)。

    基本概念是通过一个 SQL 查询来获取已经排序并带有深度指示器的节点。从那里开始,这只是一个通过循环或递归呈现输出的问题,因此将其转换为 ruby​​ 应该很容易。

    我不熟悉 awesome_nested_set 插件,但它可能已经包含一个选项来获取深度注释的有序结果,因为它是处理嵌套集时非常标准的操作/需要。

    【讨论】:

      【解决方案3】:

      自 2009 年 9 月以来,真棒嵌套集包括一种特殊方法来执行此操作: https://github.com/collectiveidea/awesome_nested_set/commit/9fcaaff3d6b351b11c4b40dc1f3e37f33d0a8cbe

      此方法比调用级别更有效,因为它不需要任何额外的数据库查询。

      示例:Category.each_with_level(Category.root.self_and_descendants) 做 |o, level|

      【讨论】:

        【解决方案4】:

        你必须递归地渲染一个会调用自身的部分。像这样的:

        # customers/show.html.erb
        <p>Name: <%= @customer.name %></p>
        <h3>Categories</h3>
        <ul>
          <%= render :partial => @customer.categories %>
        </ul>
        
        # categories/_category.html.erb
        <li>
          <%= link_to category.name, category %>
          <ul>
            <%= render :partial => category.children %>
          </ul>
        </li>
        

        这是 Rails 2.3 代码。您必须在此之前调用路由并明确命名部分。

        【讨论】:

        • 是的,我自己也想出了同样的解决方案。问题是每次调用children 都会执行一个额外的 SQL 查询(100 个子树 = 100 个 SQL 查询)。导致经典的 N+1 问题。这正是我想要避免的。此外:第一次渲染部分调用应该类似于&lt;%= render :partial =&gt; @customer.categories.roots %&gt;
        【解决方案5】:

        _tree.html.eb

        @set = Category.root.self_and_descendants
        <%= render :partial => 'item', :object => @set[0] %>
        

        _item.html.erb

        <% @set.shift %>
        <li><%= item.name %>
        <% unless item.leaf? %>
        <ul>
          <%= render :partial => 'item', :collection => @set.select{|i| i.parent_id == item.id} %>
        </ul>
        <% end %>
        </li>
        

        您也可以对其进行排序:

          <%= render :partial => 'item', :collection => @set.select{|i| i.parent_id == item.id}.sort_by(&:name) %>
        

        但在这种情况下,您应该删除此行:

        <% @set.shift %>
        

        【讨论】:

          【解决方案6】:

          我想,由于它是为旧版本的 ruby​​ 编写的,我无法得到接受的答案。这是对我有用的解决方案:

          def tree_from_set(set)
              buf = ''
          
              depth = -1
              set.each do |node|
                  if node.depth > depth
                      buf << "<ul><li>#{node.title}"
                  else
                      buf << "</li></ul>" * (depth - node.depth)
                      buf << "</li><li>#{node.title}"
                  end
          
                  depth = node.depth
              end
          
              buf << "</li></ul>" * (depth + 1)
          
              buf.html_safe
          end
          

          通过使用可选深度信息来简化它。 (这种方法的优点是输入集不需要是叶子的整个结构。)

          没有深度的更复杂的解决方案可以在 gem 的 github wiki 上找到:

          https://github.com/collectiveidea/awesome_nested_set/wiki/How-to-generate-nested-unordered-list-tags-with-one-DB-hit

          【讨论】:

            【解决方案7】:

            可能有点晚了,但我想根据closure_tree gem 嵌套hash_tree 方法分享awesome_nested_set 的解决方案:

            def build_hash_tree(tree_scope)
              tree = ActiveSupport::OrderedHash.new
              id_to_hash = {}
            
              tree_scope.each do |ea|
                h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
                (id_to_hash[ea.parent_id] || tree)[ea] = h
              end
              tree
            end
            

            这将适用于lft 订购的任何范围

            比使用助手渲染它:

            def render_hash_tree(tree)
              content_tag :ul do
                tree.each_pair do |node, children|
                  content = node.name
                  content += render_hash_tree(children) if children.any?
                  concat content_tag(:li, content.html_safe)
                end
              end
            end
            

            【讨论】:

              猜你喜欢
              • 2019-07-10
              • 1970-01-01
              • 1970-01-01
              • 2015-09-19
              • 1970-01-01
              • 1970-01-01
              • 2018-05-27
              • 1970-01-01
              • 2021-10-01
              相关资源
              最近更新 更多