Drzewo kategorii – funkcja rekurencyjna

styczeń 3rd, 2009

W wielu serwisach mamy do czynienia ze złożoną strukturą kategorii, często nie jesteśmy w stanie z góry założyć ile będzie zagłębień podkategorii. Załóżmy, że chcemy zbudować w takim serwisie. Sprawa niby banalna, jednak nadal znajduję w sieci funkcje, które wywołują zapytania do bazy danych w pętli. Widziałem nawet rozwiązania korzystające z rekurencji, tyle że każde wywołanie funkcji wysyłało kolejne zapytanie by pobrać podkategorie. Rozwiązania takie, jak nie trudno się domyślić, mają tragiczny wpływ na szybkość działania serwisu.

Dlatego też zamieszczam funkcję, która tylko raz wysyła zapytanie do bazy, przyda się pewnie wielu początkującym programistom. W bazie danych mam tabelę kategorie, a w niej kolumny id, rodzic_id, nazwa, gdzie rodzic_id zawiera id kategorii nadrzędnej lub 0 jeśli jest to główna kategoria.

function drzewoKategorii ($kategorie = null, $rodzicId = 0, $zaglebienie = 0, $wynik = null)
{
   if ($wynik == null)
      $wynik = array();
   if ($kategorie == null) {
      $query = "select * from kategorie order by rodzic_id, nazwa";
      $kategorie = DB::getAll($query);   
   }
   foreach ($kategorie as $kat) {
      if ($r["rodzic_id"] == $rodzicId) {
         $r["zaglebienie"] = $zaglebienie;
         $wynik[] = $kat;
         $wynik = drzewoKategorii ($kategorie, $kat["id"], ($zaglebienie+1), $wynik);
      }
   }
   return $wynik;
}

Funkcję tę możemy wywołać bez parametrów, wtedy sama pobierze wszystkie kategorie z bazy danych, jeśli chcecie podać własną tablicę kategorii, pamiętajcie by posortować ją po polu rodzic_id, w przeciwnym razie funkcja nie będzie działać prawidłowo. Drugi parametr $rodzicId pozwala na stworzenie drzewa kategorii od podanego zagłębienia – wystarczy podać id kategorii dla której chcemy pobrać drzewo podkategorii. Pozostałe parametry nie powinny być wprowadzane.

Funkcja drzewoKategorii zwraca tablicę kategorii ułożoną w kolejności odpowiadającej strukturze drzewa. Dla ułatwienia wyświetlania, każda kategoria otrzymała pole zaglebienie, które określa poziom zagłębienie podkategorii (im większe tym bardziej zakorzeniona jest podkategoria).

Oto przykład użycia naszej funkcji do wyświetlenia drzewa kategorii:

$drzewo = drzewoKategorii();
foreach ($drzewo as $galaz) {
   for ($i=0; $i<=$galaz["zaglebienie"]; $i++) {
      echo '-';
   }   
   echo $galaz["nazwa"].'<br/>';
}

Kategoria: php

17 komentarzy Napisz swój

  • 1. xajart  |  styczeń 24th, 2009 at 1:37 po południu

    Witam, chciałem sprawdzić działanie tej funkcji bo odpowiada strukturze tabel które mam stworzone w BD i sam sobie nie mogę poradzić ze zbudowaniem funkcji rekurencyjnej do wyświetlenia zagniezdzonego drzewa.

    Niestety nie działa mi ten skrypt ze względu na brak obsługi
    – DB::getAll()
    – foreach

    Mam pytanko jak to przebudowac tak by działało mi, co należy zastosowac zamiast powyższych poleceń ?

    Zamiast DB::getAll, sądzę że trzeba by było użyć mysql_query() – ale nie jestem pewny bo nie wiem jak działą to getAll.

    Zaś co do foreach – to nie mam pojęcia jak to zastąpić bo jest to jakieś rozszerzenie for ().

  • 2. admin  |  styczeń 26th, 2009 at 12:44 przed południem

    Tutaj masz przykładową funkcję getAll, DB::getAll zastąp getAll

    function getAll ($query) {
       $res = mysql_query($query);
       $ret = array();
       if (!$res)
          return $ret;
       while ($r = mysql_fetch_assoc($res))
          $ret[] = $r;
       return $ret;
    }

    Foreach’a nie musisz niczym zastępować, działa jak pętla for, tylko zamiast odwoływać się do elementów tablicy przez indeks, każdy kolejny element masz w zmiennej $galaz. Jednak jak bardzo chcesz to zastąpić for’em, to możesz zrobić tak:

    for ($i=0; $i < sizeof ($drzewo); $i++) {
       $galaz = $drzewo[$i];
       ...
    }
  • 3. gj  |  maj 13th, 2009 at 11:44 przed południem

    U mnie przy wynikiem skryptu jest lista z tabelki bez drzewiastej struktury. Wpierw nazwy z kategorii głównej, pod nimi po kolei z następnej (zagłębienia). Nie wiem co robię źle.

  • 4. admin  |  maj 13th, 2009 at 12:49 po południu

    Sprawdź czy na pewno zgadzają się nazwy pól w tabeli z nazwami pól w funkcji, może trzeba zastąpić np. “rodzic_id” nazwą kolumny w bazie. Jeśli nie o to chodzi to wrzuć strukturę tabelki, dane z tabelki kategorii i kod, którego używasz. W ciemno ciężko zgadywać.

  • 5. miamaji  |  czerwiec 3rd, 2009 at 11:32 przed południem

    Niestety nastąpiło u mnie przepełnienie stosu
    PHP has encountered a Stack overflow

    W bazie mam 5 rekordów wie ktoś moze dlaczego?

  • 6. admin  |  czerwiec 3rd, 2009 at 11:43 przed południem

    Musiało się zapętlić, może masz źle ustawione parent_id, np. kategoria o ID = 1 ma parent_id = 1 i odwołuje się do siebie, przejrzyj dokładnie dane w tabeli, jak nic nie znajdziesz to wklej strukturę tabeli, dane z tabeli i skrypt, wtedy będzie łatwiej pomóc.

  • 7. miamaji  |  czerwiec 3rd, 2009 at 11:55 przed południem

    Kod mam kopiuj – wklej powyższy :) Przepraszam z góry za “brudzenie” w komentarzach i dziękuję za pomoc :)
    tak wygląda baza:
    ID Category_Name Parent_ID
    1 Category 1 0
    2 Category 2 0
    5 Category 1.1 1
    6 Category 2.2 2
    3 Category 2.1 2
    4 Category 2.1.1 3
    a tak skrypt, ale naprawdę kopiuj wklej

    PS Fajna stronka, brawo.

  • 8. miamaji  |  czerwiec 3rd, 2009 at 11:56 przed południem

    function drzewoKategorii ($kategorie = null, $rodzicId = 0, $zaglebienie = 0, $wynik = null)
    {
    if ($wynik == null)
    $wynik = array();
    if ($kategorie == null) {
    $query = “SELECT * FROM dbo.Dashboard_Category ORDER BY Parent_ID, Category_Name”;
    $kategorie = getAll($query);
    }
    foreach ($kategorie as $kat) {
    if ($r["rodzic_id"] == $rodzicId) {
    $r["zaglebienie"] = $zaglebienie;
    $wynik[] = $kat;
    $wynik = drzewoKategorii ($kategorie, $kat["id"], ($zaglebienie+1), $wynik);
    }
    }
    return $wynik;
    }

    function getAll ($query) {
    $res = mssql_query($query);
    $ret = array();
    if (!$res)
    return $ret;
    while ($r = mssql_fetch_assoc($res))
    $ret[] = $r;
    return $ret;
    }

  • 9. admin  |  czerwiec 3rd, 2009 at 12:23 po południu

    Pierwsze co się rzuca w oczy to inna nazwa kolumny w tabeli (Parent_ID) a inna w funkcji (rodzic_id), zmień to w funkcji bo to chyba tu leży problem. Zaśmiecać nie zaśmiecasz, od tego są komentarze ;) Napisz czy pomogło, jak coś to będziemy szukać dalej.

  • 10. miamaji  |  czerwiec 3rd, 2009 at 12:31 po południu

    Niestety błąd jest nadal. A może błędu doszukiwać się MSSQL?

  • 11. admin  |  czerwiec 3rd, 2009 at 1:00 po południu

    odpal poza funkcją te linijki:
    $query = “SELECT * FROM dbo.Dashboard_Category ORDER BY Parent_ID, Category_Name”;
    $kategorie = getAll($query);
    to zobaczysz czy błąd jest w bazie czy nie, ale raczej nie, popraw jeszcze
    $wynik = drzewoKategorii ($kategorie, $kat["id"], ($zaglebienie+1), $wynik);
    na
    $wynik = drzewoKategorii ($kategorie, $kat["ID"], ($zaglebienie+1), $wynik);

    jeśli nie pomogło to odpal ten kod i wklej mi wynik:
    $query = “SELECT * FROM dbo.Dashboard_Category ORDER BY Parent_ID, Category_Name”;
    $kategorie = getAll($query);
    echo ”,print_r($kategorie),”;

  • 12. admin  |  czerwiec 3rd, 2009 at 1:01 po południu

    oj wycięło mi tagi html’owe, dobra to wpisz samo print_r($kategorie); w ostatniej linijce.

  • 13. miamaji  |  czerwiec 3rd, 2009 at 1:36 po południu

    DZIAŁA ! Nie pozostaje mi nic jak tylko ładnie podziękować.

    DZIĘKUJE :)

  • 14. miamaji  |  czerwiec 3rd, 2009 at 1:38 po południu

    albo i nie :(

    -Category 1
    -Category 2
    -Category 2.1
    -Category 2.2
    -Category 2.1.1

    dostaje taki wynik

  • 15. Zyx  |  czerwiec 3rd, 2009 at 1:49 po południu

    Ech, co z tego, że wywołujesz zapytanie jeden raz, kiedy dalej masz rekurencję, i to nienajlepiej napisaną? Kilka wad samego rozwiązania:

    1. Chcesz pobrać fragment drzewa, musisz pobrać całe drzewo.
    2. Głębokość drzewa jest ograniczona przez sztywne limity PHP głębokości stosu, które przecież nie idą tylko na rekurencję, ale też na wywołania innych funkcji. A jeśli się go przekroczy, to tak… “Nesting level too deep” i działanie skryptu przerwane.
    3. W szukaniu ścieżki od danego węzła do korzenia także nie ma lekko – prawie wszystkie pobrane węzły są niepotrzebne, tylko obciążają skrypt.

    Ogólnie rozwiązanie ma jedną, poważną wadę: robi problemy tam, gdzie ich nie ma. Przecież to wszystko potrafi zrobić sama baza danych, tylko należy pomyśleć i jej na to pozwolić, a nie bawić się w wyważanie otwartych drzwi w imię nie wiadomo czego. Są dużo lepsze, dużo wydajniejsze algorytmy do obsługi i przechowywania drzew w bazie, a że wymagają pewnej zmiany nawyków i sposobu myślenia – cóż, nikt nie powiedział, że dobre rozwiązania muszą być łatwe i przyjemne.

    Mam też sporo uwag co do zaprezentowanego kodu:

    1. Czemu nie używasz referencji do pamiętania tablicy z wynikiem?
    2. Po co Ci zmienna tymczasowa do zapisywania zapytania? Przecież nic z nim później nie robisz.
    3. Porównanie z typem null powinno być robione operatorem ===. W przeciwnym wypadku może zostać złapane nawet coś, co w rzeczywistości nullem nie jest.
    4. str_repeat() – do ad. rysowania pauz pętlą.

  • 16. admin  |  czerwiec 3rd, 2009 at 2:29 po południu

    @miamaji: to wynik zapytania do bazy czy wywołania funkcji?
    @Zyx: dzięki za uwagi, fakt, że niedługo po publikacji tego wpisu zacząłem borykać się z serią problemów wynikającą z takiej struktury kategorii. o ile przy prostych projektach nie jest źle, to jak dochodzi do konieczności przeszukiwania gałęzi to zaczynają się schody.

  • 17. miamaji  |  czerwiec 3rd, 2009 at 2:33 po południu

    Wynik zapytania z funkcji

Napisz komentarz

Required

Required, nie jest publikowany

Dozwolone tagi HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Nawiąż do wpisu  |  śledź komentarze przez RSS


Kalendarz

luty 2012
P W Ś C P S N
« stycznia    
 12345
6789101112
13141516171819
20212223242526
272829  

Ostatnie wpisy