Как отобразить все записи из вложенного набора в реальное дерево html

я использую awesome_nested_set плагин в моем проекте Rails. У меня есть две модели, которые выглядят так (упрощенно):

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, lft и rgt правильные. Дерево имеет несколько корневых узлов (что, конечно, разрешено в awesome_nested_set).

теперь я хочу отобразить все категории данного клиента в правильно отсортированном дереве, таком как структура: например, вложенный <ul> метить. Это было бы не слишком сложно, но мне нужно, чтобы это было эффективно (чем меньше sql-запросов, тем лучше).

обновление: выяснил, что можно вычислить количество детей для любого заданного узла в дереве без дальнейших SQL-запросов:number_of_children = (node.rgt - node.lft - 1)/2. Это не решает проблему, но может оказаться полезным.

7 ответов


было бы неплохо, если бы вложенные наборы имели лучшие функции из коробки, не так ли?

трюк, как вы обнаружили, состоит в том, чтобы построить дерево из плоского набора:

  • начните с набора всех узлов, отсортированных по 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 

Я ответил аналогичный вопрос для php недавно (вложенный набор = = измененная модель обхода дерева предзаказа).

основная концепция состоит в том, чтобы получить узлы уже упорядоченные и с индикатором глубины с помощью один SQL-запрос. Оттуда это просто вопрос рендеринга вывода через цикл или рекурсию, поэтому его должно быть легко преобразовать в ruby.

Я не знаком с awesome_nested_set plug in, но он уже может содержать опцию получите аннотированный, упорядоченный результат глубины, так как это довольно стандартная операция/необходимость при работе с вложенными наборами.


с сентября 2009 года awesome вложенный набор включает в себя специальный метод для этого: https://github.com/collectiveidea/awesome_nested_set/commit/9fcaaff3d6b351b11c4b40dc1f3e37f33d0a8cbe

этот метод намного эффективнее, чем вызов уровня, потому что он не требует дополнительных запросов к базе данных.

Пример: Категория.each_with_level(категория.корень.self_and_descendants) do|o, level/


вы должны рекурсивно отобразить частичное, которое будет вызывать себя. Что-то вроде этого:--2-->

# 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. Вы должны будете назвать маршруты и назвать частичное явно до этого.


_tree.формат html.eb

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

_item.формат html.Эрб

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

Я не мог получить принятый ответ из-за старой версии 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

это упрощается с помощью дополнительно подробную информацию. (Преимущество такого подхода заключается в том, что нет необходимости для ввода всей структуре листьев.)

более сложное решение без глубины можно найти на GitHub wiki джем:

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


может быть, немного поздно, но я хотел бы поделиться мое решение для awesome_nested_set на основе closure_tree gem вложенные 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

чем использовать helper для его визуализации:

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