2009-09-03 8 views
8

Sto utilizzando il plug-in awesome_nested_set nel mio progetto Rails. Ho due modelli che assomigliano a questo (semplificato):Come eseguire il rendering di tutti i record da un set nidificato in un albero html reale

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'albero nel database è costruito come previsto. Tutti i valori di parent_id, lft e rgt sono corretti. L'albero ha più nodi radice (che è ovviamente consentito in awesome_nested_set).

Ora, voglio eseguire il rendering di tutte le categorie di un determinato cliente in una struttura ad albero ordinata correttamente: ad esempio i tag nidificati <ul>. Questo non sarebbe troppo difficile, ma ho bisogno che sia efficiente (meno query SQLl, meglio è).

Aggiornamento: È stato calcolato che è possibile calcolare il numero di figli per ogni dato nodo nell'albero senza ulteriori query SQL: number_of_children = (node.rgt - node.lft - 1)/2. Questo non risolve il problema ma potrebbe rivelarsi utile.

risposta

7

Sarebbe bello se gli insiemi nidificati avessero funzionalità migliori fuori dalla scatola, no?

Il trucco, come avete scoperto è quello di costruire l'albero da un set piatta:

  • inizio con una serie di tutti i nodi ordinati per LFT
  • il primo nodo è una radice aggiungerlo come la radice dell'albero si sposta sul nodo successivo
  • se è un figlio del nodo precedente (lft tra prev.lft e prev.rht) aggiungi un figlio all'albero e sposta in avanti un nodo
  • altrimenti sposta l'albero uno test di livello e ripetizione

vedere di seguito:

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

Questo funziona. Hai ragione anche su awesome_nested_set. Non posso fare a meno di chiedermi perché questo non sia integrato nel plugin in primo luogo. Grazie! –

+0

Hai dimenticato di menzionare: il punto essenziale della tua soluzione è che richiede una sola query SQL! –

+3

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

3

Devi rendere in modo ricorsivo un partial che chiamerà se stesso. Qualcosa del genere:

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

Questo è il codice Rails 2.3. Dovrai chiamare i percorsi e nominare la parte in modo esplicito prima di quello.

+1

Sì, mi si avvicinò con la stessa soluzione io stesso. Il problema è che OGNI chiamata a 'children' esegue una query SQL aggiuntiva (100 subtrees = 100 query SQL). Risultati in un classico problema N + 1. Questo è esattamente quello che sto cercando di evitare. Inoltre: la prima chiamata parziale di rendering dovrebbe essere qualcosa come '<% = render: partial => @ customer.categories.roots%>' –

5

Ho risposto a un similar question for php recentemente (nested set == modello di attraversamento dell'albero preordinato modificato).

Il concetto di base è di ottenere i nodi già ordinati e con un indicatore di profondità tramite una query SQL. Da lì è solo una questione di rendere l'output tramite loop o ricorsione, quindi dovrebbe essere facile convertirlo in ruby.

Non ho familiarità con il plug-in awesome_nested_set, ma potrebbe già contenere un'opzione per ottenere il risultato con annotazione della profondità, ordinato, in quanto è un'operazione/necessità piuttosto standard quando si tratta di set annidati.

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> 

È inoltre possibile ordinare la loro:

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

ma in questo caso si dovrebbe rimuovere questa riga:

<% @set.shift %> 
1

Non riuscivo a trovare la risposta accettata a causa della vecchia versione di rubino per cui è stato scritto, suppongo. Ecco la soluzione di lavoro per me:

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 

E 'semplificato utilizzando le informazioni di profondità opzionale. (vantaggio di questo approccio è che non è necessario per l'ingresso impostato per essere l'intera struttura alle foglie.)

soluzione più complessa senza profondità possono essere trovate sul github wiki della gemma:

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

0

Forse un po 'tardi, ma mi piacerebbe condividere la mia soluzione per awesome_nested_set basata su closure_tree gemma annidati hash_tree metodo:

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 

Questo funziona con qualsiasi ambito ordinato da lft

che utilizzare helper per renderla:

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