2009-09-03 21 views
8

Estoy usando el complemento awesome_nested_set en mi proyecto Rails. Tengo dos modelos que se ven así (simplificada):Cómo representar todos los registros de un conjunto anidado en un árbol html real

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 

El árbol en la base de datos se construye como se esperaba. Todos los valores de parent_id, lft y rgt son correctos. El árbol tiene múltiples nodos raíz (que, por supuesto, está permitido en awesome_nested_set).

Ahora, quiero representar todas las categorías de un cliente dado en una estructura tipo árbol correctamente ordenada: por ejemplo, etiquetas <ul> anidadas. Esto no sería demasiado difícil, pero necesito que sea eficiente (cuanto menos SQL consultas, mejor).

Actualización: Se descubrió que es posible calcular el número de hijos para cualquier nodo dado en el árbol sin más consultas SQL: number_of_children = (node.rgt - node.lft - 1)/2. Esto no resuelve el problema, pero puede ser útil.

Respuesta

7

Sería bueno si los conjuntos anidados tienen mejores características fuera de la caja, ¿no?

El truco como usted ha descubierto es construir el árbol de un conjunto plana:

  • comienzo con un conjunto de todos los nodos ordenados por lft
  • el primer nodo es una raíz añadirlo como la raíz del movimiento del árbol al siguiente nodo
  • si es un hijo del nodo anterior (LFT entre prev.lft y prev.rht) añadir un niño al árbol y avanzar un nodo
  • moverse de otro modo el único árbol nivel y repita la prueba

ver a continuación:

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

Esto funciona. Tienes razón con respecto a awesome_nested_set también. No puedo evitar preguntarme por qué esto no está incorporado en el complemento en primer lugar. ¡Gracias! –

+0

Se olvidó de mencionar: ¡El punto esencial acerca de su solución es que requiere una sola consulta SQL! –

+3

http://gist.github.com/460814 –

3

Tienes que recursivamente renderizar un parcial que se llamará a sí mismo. Algo como esto:

# 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> 

Este es el código de Rails 2.3. Tendrá que llamar a las rutas y nombrar el parcial explícitamente antes de eso.

+1

Sí, me ocurrió con la misma solución a mí mismo. El problema es que CADA invocación a 'children' ejecuta una consulta SQL adicional (100 subárboles = 100 consultas SQL). Resulta en un problema clásico de N + 1. Eso es exactamente lo que trato de evitar. Además: la primera llamada parcial de renderizado debe ser algo así como '<% = render: partial => @ customer.categories.roots%>' –

5

Respondí a similar question for php recientemente (conjunto anidado == modelo de árbol de preorden modificado).

El concepto básico es obtener los nodos ya ordenados y con un indicador de profundidad por medio de una consulta SQL. A partir de ahí, solo se trata de renderizar la salida mediante bucle o recursión, por lo que debería ser fácil convertir esto en ruby.

No estoy familiarizado con el awesome_nested_set plug-in, pero puede que ya contenga una opción para obtener la profundidad anotada, resultado ordenado, ya que es una operación/necesidad bastante estándar cuando se trata de conjuntos anidados.

3

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

También puede ordenar su:

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

pero en ese caso se debe eliminar esta línea:

<% @set.shift %> 
1

No pude ponerme a trabajar la respuesta aceptada debido a la versión anterior de ruby ​​para la que fue escrita, supongo. Aquí está la solución de trabajo para mí:

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 

Se ha simplificado mediante el uso de la información de profundidad opcional. (La ventaja de este enfoque es que no hay necesidad de que el conjunto de entrada para ser toda la estructura de las hojas.)

solución más compleja, sin profundidades se puede encontrar en GitHub wiki de la gema:

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

0

Tal vez un poco tarde, pero me gustaría compartir mi solución para awesome_nested_set basado en closure_tree joya anidados hash_tree método:

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 

Este funciona con cualquier ámbito ordenado por lft

que el uso de ayuda para hacerlo:

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 
Cuestiones relacionadas