locknet.ro

archive

Nested Set 2 Unordered List

Da, trebuie sa recunosc, de cateva zile lucrez (as freelance) la un proiect web ce va folosi Medick.
Experienta aceasta va imbunatati cu siguranta calitatea frameworkului, in primul rand pentru ca in felul asta pot adauga noi facilitati – vad mai bine lipsurile – apoi, am o motivatie in plus: voi primi o suma de bani pentru proiectul respectiv.

Aplicatiea este destul de simpla: “un fel de” magazin, momentan doar de prezentare a produselor.
Aceste produse sunt caracterizate de un producator (has_one company) si de o categorie in care sunt inscrise (has_one category), pe langa proprietati simple de tipul pret, cantitate, nume sau descriere.

Am ales sa folosesc modelul Nested Set pentru a stoca arborele categoriilor tinand cont si de “target database”: MySQL 4.1.
Un articol foarte bun: Managing Hierarchical Data in MySQL de Mike Hillyer dar si prezentarea sa cu acelasi nume sustinuta la PHP Quebec 2005.
Arborele meu are un nod central numit root (exact ca in exemplul prezentat in articolul de mai sus, nod denumit 'ELECTONICS'), insa acest nod nu va fi afisat niciodata.

Cea mai mare bucurie a acestui proiect - pana acum - a fost data de transformarea arborelui rezultat in urma unui SELECT intr-o lista ne-ordonata, folosita, de exemplu, pentru a afisa un “site-map” sau pentru afisarea unui meniu de navigare.

De doua zile stau si admir rezultatul si avantajele acestui model:
un SQL SELECT si nici o recursivitate!

Selectul:

SELECT node.*, (COUNT(parent.name) - 1) AS depth
  FROM categories AS node, categories AS parent 
WHERE
  node.lft BETWEEN parent.lft AND parent.rgt
  AND parent.id !=1 
GROUP BY node.name ORDER BY node.lft;

depth reprezinta “adancimea” in arbore a nodului curent sau distanta fata de primul nod (acesta are “adancime” 0)
parent.id!=1 pentru ca nu vreau primul nod (root) in lista rezultata.

Rezultatul este un ResultSet produs de creole.

Parcurgerea:

$last_depth = -1;
$output= '<ul>';  // rezultatul final
while($rs->next()) {
  $current_depth = $rs->getInt('depth'); // adancimea nodului curent
  // conditie cheie:
  if ($last_depth > $current_depth) {
    $output .= str_repeat('</ul></li>', ($last_depth - $current_depth));
  }
  $output .= '<li>' . $rs->getString('name');
  // conditie echivalenta cu intrebarea hasChildren()?,
  // urmata de getChilds() si parcurgerea recursiva 
  // a noului arbore
  if ($rs->getInt('rgt') - 1 != $rs->getInt('lft')) {
    $output .= '<ul>';
  } else {
    $output .= '</li>';
  }
  $last_depth = $current_depth;
}
echo $output . '</ul>';

Unde mai pui ca rezultatul este valid XHTML.

Lectura suplimentara:

Working with Graphs in MySQL (vezi si biografia atasata)
Joe Celko's Trees and Hierarchies in SQL for Smarties