PHP feat. MySQL: Sposób na drzewka
aktualizacja
W Sieci można natknąć się na wiele przepisów, w jakis sposób stworzyć drzewka zapisywane w DB. Niektóre mają więcej, niektóre mniej wad… Ale poszukiwałem jakiegoś w miarę uniwersalnego rozwiązania, które by najlepiej pasowało do moich potrzeb.
Założenia
Potrzebujemy drzewka kategorii. Ot, takie najprostsze, np:
- kategoria
- podkategoria1
- podkategoria2
- kategoria
Ponieważ zachodziłem w głowę, co by tu można było zrobić, budowanie drzewka zachodziło bez problemów, kod się walidował, postanowiłem – na dzień dobry – poćwiczyć na tablicach i metodą notatek/prób i błędów dojść do rozwiązania.
Przygotowania
Bazowałem na poniższej tablicy:
$d[] = array('asdasd1-', 1);
$d[] = array('asdasd2-', 1);
$d[] = array('asdasd3-', 2);
$d[] = array('asdasd4-', 2);
$d[] = array('asdasd5-', 1);
$d[] = array('asdasd6-', 2);
$d[] = array('asdasd7-', 2);
$d[] = array('asdasd8-', 1);
$d[] = array('asdasd9-', 2);
$d[] = array('asdasd10-', 1);
Pierwsze pole w tablicy każdego rekordu, to nazwa, a drugie – stopień zagłębienia w strukturze. O tym później.
Początkowo, próbowałem obrabiać to za pomocą zwykłej pętli for. Errr, nie. Dlaczego? O ile w przypadku tablic nie będzie raczej problemów wydajnościowych przy liczeniu elementów tablicy, to podczas korzystania z bazy już mogą się pojawić takie problemy. Wiem, że rzadko mogą się zdarzyć takie duuuuże drzewa, ale… Każda ms jest ważna. ;]
Więc co zrobić? Pozostaje już tylko pętla while.
Po co więc nam drugi parametr w tablicy? Otóż – jak już wcześniej wspomniałem – posłuży on nam do określenia, jak głęboko w hierarchicznej strukturze drzewa znajduje się dany wpis. Stąd – aby poprawnie rozpoczynać i kończyć listy wyliczeniowe – musimy sprawdzać, czy następny element jest większy bądź mniejszy od obecnie przetwarzanego. Upraszczając, jeśli liczba jest większa od bieżącej – rozpocznij nową podkategorię, jeśli mniejsza – zamknij podkategorię, jeśli taka sama – wypisz element.
W przypadku pętli for tego problemu za bardzo nie ma, ponieważ możemy użyć konstrukcji $d[$a+1], aby dostać się do następnego elementu. Przy while musimy sobie pomóc.
Po prostu, będziemy pobierać dwa rekordy za jednym razem.
…i praktyka.
Aby nie pobierać kilkukrotnie tego samego rekordu, utworzymy sobie dwa bufory.
$start = true;
$buff1 = null;
$buff2 = null;
Z każdym przejściem pętli będą się one przesuwały cyklicznie o jeden (z drugiego do pierwszego; do pierwszego zapisany nowy element).
Początek pętli wygląda mniej więcej tak:
while(list($key,$r) = each($d)){
if($start){
$buff1 = $r;
$buff2 = $d[1];next($d);
$start = false;
}else{
$buff1 = $buff2;
$buff2 = $r;
}
Zmienną pomocniczą jest tu $start, która służy do sprawdzenia, czy jest to pierwszy przebieg pętli. Umożliwia to prawidłową wymianę danych pomiędzy buforami.
W celu zwiększenia przejrzystości kodu (dla celów tego wpisu!), tworzymy referencje do odpowiednich elementów tablicy, które zawierają dane o stopniu zagłębienia.
$curr = &$buff1[1];
$next = &$buff2[1];
I teraz obrabiamy poszczególne elementy.
//rozpocznij element i wypisz wartość
echo '
//jeśli istnieje następny element...
if($next!==null){
//jeśli następny element jest głębiej, utwórz nową listę
if($next>$curr){
echo '
- ';
}
//jeśli następny element jest płycej, kończymy listę...
if($next<$curr){
//...odpowiednią ilość razy - przeliczanie w przypadku bardziej zagłębionych list
for($x=0;$x<($curr-$next);$x++){
echo '
';
}
}
//jeśli następny element jest na takiej samej głębokości, uzupełnij element listy
if($next==$curr){
echo '
';
}
}else{
//jeśli to ostatni...
echo '
';
}
Oczywiście, nie zapomnij o ujęciu całego kodu w znaczniki <ul></ul>.
MySQL
W kodzie za bardzo się różnić nie będzie w porównaniu to edycji tablicowej. Wystarczy edycja, nazwijmy to „nagłówka pętli”.
while($r = $q->getRow()){
if($start){
$buff1 = $r;
$buff2 = $q->getRow();
$start = false;
}else{
$buff1 = $buff2;
$buff2 = $r;
}
Oczywiście, funkcję $q->getRow() zamień na odpowiednią, używaną w Twoim skrypcie. Konieczna będzie również zmiana odwołań do poszczególnych tablic ($d[0], itp. zamień na odpowiadające Twojej tabeli w bazie).
Sama tabela może wyglądać mniej więcej tak:
- ID unsigned INT – dla potrzeb zarządzania
- depth unsigned INT – głębokość w hierarchii
- title char(255) – tytuł
- page unsigned INT – ewentualne odniesienie do podstrony w serwisie
Dlaczego by nie sprzężyć tej tabeli z zawartością stron? To temat, który został już rozklepany.
O czym powinniśmy pamiętać?
Używaj kolejno następujacych po sobie poziomów zagłębienia. Np. jeśli nadrzędnym jest 1, to następny powinien być 2, a nie wyższy. Dlaczego? Raz: aby uniknąć chaosu, dwa: wówczas wymagałoby to użycia kolejnej pętli w kodzie (analogicznie jak to ma miejsce w przypadku </ul></li>).
Aktualizacja
Powyższy kod działa, OK. Ale ucina ostatni element… Gdyby liczba wpisów w tablicy/tabeli była nieparzysta, to wszystko by pięknie działało. Jest parzysta, ups – ucięło… Posiedziałem przy serniku i herbacie, udało mi się z tym poradzić.
Przyda się drugi rodzaj pętli while, ten rzadziej wykorzystywany. Cała pętla wygląda tak (reszta bez zmian):
do{
list($key,$r) = each($d);
if($start){
$buff1 = $r;
$buff2 = $d[1];next($d);
$start = false;
}else{
$buff1 = $buff2;
$buff2 = $r;
}
if($buff1==null){
break;
}
$curr = $buff1[1];
$next = $buff2[1];
echo '
if($next!==null){
if($next>$curr){
echo '
- ';
}
if($next<$curr){ for($x=0;$x<($curr-$next);$x++){ echo '
';
}
}
if($next==$curr){
echo '
';
}
}else{
echo '
';
}
}while($buff1!=null);
EOF;
Jeśli chodzi o prostotę i łatwość implementacji, to pod tym względem udało Ci się znakomicie. Przechowywanie informacji o kolejności i głębokości umożliwia łatwe zarządzanie strukturą drzewa i jego wyświetlanie, lecz odbija się to kosztem możliwości. Gdyby takie drzewko chcieć użyć w CMS-ie, zakodowanie pobierania wielu podstawowych informacji byłoby bardzo problematyczne:
1. Określanie przodków wybranego węzła – MySQL musi zassać do skryptu dużą ilość niepotrzebnych danych, które ponadto trzeba specjalnie obrabiać, aby dostać, co trzeba.
2. Wyświetlanie fragmentu drzewa – od strony kodowej proste, natomiast MySQL pobierze do skryptu zdecydowanie za dużo niepotrzebnych danych. Z drugiej strony łatwo wyświetlić drzewo do określonej głębokości.
3. Zliczanie potomków.
Istnieje algorytm przechodzenia drzewa zmodyfikowaną metodą preorder – można z niego przy pobieraniu całkiem prosto wyliczyć głębokość za pomocą stosu, a dzięki niemu opisane powyżej rzeczy stają się proste do wykonania. Niestety, zarządzanie takim drzewkiem wymaga więcej wysiłku; w dodatku i tak wypada to połączyć z jeszcze inną metodą. Osobiście dodaję do tego jeszcze zwyczajnie pole parent_id, aby zawsze można było się łatwo dobrać bezpośrednio do rodzica czy wyświetlić wszystkie dzieci. Tak więc do poważnych zastosowań przeważnie trzeba połączyć kilka metod naraz, aby uzyskać zamierzony efekt.