2009-09-03 5 views
8

J'utilise le plugin awesome_nested_set dans mon projet Rails. J'ai deux modèles qui ressemblent à ceci (simplifié):Comment rendre tous les enregistrements d'un ensemble imbriqué dans un arbre html réel

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 

L'arbre dans la base de données est construite comme prévu. Toutes les valeurs de parent_id, lft et rgt sont correctes. L'arbre a plusieurs nœuds racines (ce qui est bien entendu autorisé dans awesome_nested_set).

Maintenant, je veux rendre toutes les catégories d'un client donné dans une arborescence triée correctement comme structure: par exemple <ul> étiquettes imbriquées. Ce ne serait pas trop difficile mais j'ai besoin d'être efficace (le moins de requêtes SQL le mieux).

Mise à jour: Calculé qu'il est possible de calculer le nombre d'enfants pour un nœud donné dans l'arborescence sans autres requêtes SQL: number_of_children = (node.rgt - node.lft - 1)/2. Cela ne résout pas le problème, mais cela peut s'avérer utile.

Répondre

7

Ce serait bien si les ensembles imbriqués avaient de meilleures fonctionnalités hors de la boîte ne le feraient pas.

L'astuce que vous avez découvert est de construire l'arbre d'un ensemble plat:

  • départ avec un ensemble de tous les nœuds triés par LFT
  • le premier noeud est une racine ajoutez comme la racine de l'arbre passer au nœud suivant
  • s'il s'agit d'un enfant du nœud précédent (entre prev.lft et prev.rht) ajouter un enfant à l'arbre et avancer d'un nœud
  • sinon remonter l'arbre niveau et répéter le test

voir ci-dessous:

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

Cela fonctionne. Vous avez raison concernant awesome_nested_set aussi. Je ne peux pas m'empêcher de me demander pourquoi ce n'est pas intégré dans le plugin en premier lieu. THX! –

+0

Oublié de mentionner: Le point essentiel à propos de votre solution est qu'il ne nécessite qu'une seule requête SQL! –

+3

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

3

Vous devez rendre récursivement un partiel qui s'appellera. Quelque chose comme ceci:

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

Ceci est le code de Rails 2.3. Vous devrez appeler les routes et nommer le partiel explicitement avant cela.

+1

Ouais, je suis venu avec la même solution moi-même. Le problème est que chaque appel à 'children' exécute une requête SQL supplémentaire (100 sous-arbres = 100 requêtes SQL). Résultats dans un problème N + 1 classique. C'est exactement ce que j'essaie d'éviter. En outre: Le premier appel partiel de rendu devrait être quelque chose comme '<% = render: partial => @ customer.categories.roots%>' –

5

J'ai récemment répondu à un similar question for php (modèle imbriqué == modifié de traversée de l'arbre de pré-commande).

Le concept de base est d'obtenir les nœuds déjà commandés et avec un indicateur de profondeur au moyen de une requête SQL. De là, il s'agit simplement de rendre la sortie via une boucle ou une récursion, donc il devrait être facile de convertir ceci en ruby.

Je ne suis pas familier avec le awesome_nested_set brancher, mais il peut déjà contenir une option pour obtenir la profondeur annotée, résultat ordonné, comme c'est une opération assez standard/besoin lorsqu'il s'agit de jeux imbriqués.

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> 

Vous pouvez également trier leur:

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

mais dans ce cas, vous devez supprimer cette ligne:

<% @set.shift %> 
1

Je ne pouvais pas obtenir de travailler la réponse acceptée en raison de l'ancienne version de ruby ​​il a été écrit pour, je suppose. Voici la solution de travail pour moi:

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 

Il est simplifié à l'aide du en option information de profondeur. (avantage de cette approche est qu'il n'y a pas besoin de l'ensemble d'entrée pour être toute la structure des feuilles.)

solution plus complexe sans profondeur se trouve sur le wiki github de la gemme:

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

0

Peut-être un peu en retard mais je voudrais partager ma solution pour awesome_nested_set basée sur gem closure_tree imbriquées méthode 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 

Cela fonctionnera avec une portée commandée par lft

Than aide de l'utilisation pour le rendre:

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 
Questions connexes