如何将所有记录从嵌套集合转换为真正的html树

问题描述:

我在我的Rails项目中使用awesome_nested_set插件。我有两个看起来像这样的模型(简化版):如何将所有记录从嵌套集合转换为真正的html树

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_id,lftrgt的所有值都是正确的。该树有多个根节点(当然允许在awesome_nested_set中)。

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

更新:找出有可能计算没有进一步SQL查询的树中任何给定节点的孩子数:number_of_children = (node.rgt - node.lft - 1)/2。这并不能解决问题,但它可能被证明是有帮助的。

如果嵌套套件具有更好的功能,那就好了。

因为你已经发现了这个窍门是从平面组建立树:

  • 开始与LFT排序的集合中的所有节点的
  • 的第一个节点是根将其添加为根树移动到下一个节点
  • 如果是前一个节点的孩子(prev.lft和prev.rht之间LFT)添加子树和向前移动一个节点
  • 否则移树一个水平和重复测试

见下图:

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 
+0

这可以工作。你对awesome_nested_set也是对的。我忍不住想知道为什么这不是首先嵌入到插件中的。谢谢! – 2009-09-05 18:10:43

+0

忘了提及:关于您的解决方案的要点是,它只需要一个SQL查询! – 2009-09-05 18:11:58

+3

http://gist.github.com/460814 – 2010-07-02 01:41:55

你必须递归渲染一个将自己调用的部分。类似这样的:

# 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的代码。您必须在此之前调用路线并明确命名部分。

+1

是啊,我想出了相同的解决方案我自己。问题是每次调用'children'都会执行额外的SQL查询(100个子树= 100个SQL查询)。导致经典的N + 1问题。这正是我想要避免的。此外:第一个渲染部分调用应该像' @ customer.categories.roots%>' – 2009-09-03 12:36:30

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

基本概念是通过一个SQL查询得到已经订购的节点和深度指示符。从那里它只是通过循环或递归呈现输出的问题,所以应该很容易将其转换为ruby。

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

_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 %> 

自2009年9月真棒嵌套组包括一个特殊的方法做这个: https://github.com/collectiveidea/awesome_nested_set/commit/9fcaaff3d6b351b11c4b40dc1f3e37f33d0a8cbe

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

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

我不能去接受的答案,因为旧版本的红宝石是写的,我想。下面是该解决方案为我工作:

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 

它使用可选深度信息简化。 (这种方法的优点是,有没有必要的输入设置为整体结构的叶子),无深度

更复杂的解决方案可以在创业板的GitHub的wiki上找到:

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

也许有点晚了,但我想分享我的解决方案基于closure_tree宝石awesome_nested_set嵌套hash_tree方法:

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