Prawdopodobnie nieraz zdarzyło się wam budować drzewiaste struktury danych w bazach danych. Typowa tabela do tego celu wygląda mniej więcej tak:
id | parent_id | name ---+-----------+------------- 1 | 0 | Lorem 2 | 0 | Ipsum 3 | 1 | Dolor 4 | 1 | Sit 5 | 2 | Consectetuer 6 | 4 | Adipiscing 7 | 4 | Elit
Pozwala na dowolnie głębokie zagnieżdżanie danych, zaś obiekty o parent_id
równym 0 są elementami najwyższego poziomu hierarchii.
Problem pojawia się, kiedy dane te trzeba zwizualizować w postaci drzewa. Początkujący programiści często używają do tego celu rekursji (właściwie, to używają jej do wszystkiego), jednak ma ona podstawową wadę. O ile w przypadku powyższej tabeli nie ma zasadniczo różnicy, czy dane będą pobierane rekurencyjnie czy liniowo, o tyle przy tysiącu kategorii, gdzie w najwyższym poziomie znajduje się ich kilkaset, liczba zapytań wywoływanych przez skrypt sięga setek na każde wyświetlenie strony.
Jest to problemem zwłaszcza, kiedy serwer SQL znajduje się na innej maszynie i każde zapytanie wymaga przesyłania danych w obie strony połączenia. Wprowadza to dodatkowe opóźnienia, a z czasem staje się nader widoczne.
Rozwiązaniem jest jednorazowe pociągniecie wszystkich potrzebnych danych za pomocą jednego zapytania, a następnie przebudowanie w strukturę drzewiastą po stronie serwera docelowego.
Jako przykład takiego działania posłużyć może poniższy skrypt PHP:
require_once('arraySort.class.php'); /** * Create a tree structure from an SQL query */ class sqlTree { var $conn; function sqlTree(&$conn) { $this->conn = & $conn; } /** * Sort an array * @access private */ function _multiSort(&$array, $key, $asc = true) { $sorter = & new arraySort($array, $key, $asc); return $sorter->sortit(); } /** * Sort a tree * @access private */ function _treeSort(&$item) { reset($item); foreach($item as $key => $val) { if (isset($val['tree'])) $this->_treeSort($item[$key]['tree']); } $item = $this->_multiSort($item, 'sort', true); } /** * Build the tree, elements with no known parent are ignored * @param string SQL query, has to contain fields 'id' and 'parent_id' * @param int parent_id to be considered root level * @return array the tree structure */ function build($query, $root = 0) { $result = mysql_query($query, $this->conn); $queue = array(); while($item = mysql_fetch_assoc($result)) { array_push($queue, $item); } $queue = $this->_multiSort($queue, 'id', false); $effect = array(); while($queue) { $found = false; reset($queue); $nextIter = $queue; foreach($queue as $key => $item) { if ($item['parent_id'] != $root) { reset($nextIter); foreach($nextIter as $keyTarg => $target) if ($target['id'] == $item['parent_id']) { if (!isset($nextIter[$keyTarg]['tree'])) $nextIter[$keyTarg]['tree'] = array(); array_push($nextIter[$keyTarg]['tree'], $item); unset($nextIter[$key]); $found = true; break; } if ($found) break; // not found unset($nextIter[$key]); } else { array_push($effect, $item); unset($nextIter[$key]); break; } } $queue = $nextIter; } $this->_treeSort($effect); return $effect; } }
Jest to klasa, która jako parametr konstruktora pobiera identyfikator połączenia do bazy danych MySQL i udostępnia funkcję build()
, która jako pierwszy parametr przyjmuje zapytanie wybierające dane, zaś jako drugi opcjonalny identyfikator korzenia drzewa.
Wymagania odnośnie zapytania są następujące: musi ono zawierać w wynikach kolumny o nazwach id
, parent_id
i sort
, przy czym ten ostatni decyduje o kolejności pozycji drzewa. Dodatkowym ograniczeniem (powyższej implementacji) jest założenie, że dla każdego elementu zachodzi parent_id
< id
.
Przykładowe użycie:
$st = & new sqlTree($connection); $tree = $st->build( ' SELECT id, parent_id, name AS sort FROM przykladowa_tabela '); print_r($tree);
Do działania wymagany jest jeszcze plik arraySort.class.php
, czyli skrypt sortowania wielowymiarowych tablic autorstwa Oliwiera Ptaka:
/** * Handles multidimentional array sorting by a key (not recursive) * * @author Oliwier Ptak <aleczapka at gmx dot net> */ class arraySort { var $skey = false; var $sarray = false; var $sasc = true; /** * Constructor * * @access public * @param mixed $array array to sort * @param string $key array key to sort by * @param boolean $asc sort order (ascending or descending) */ function arraySort(&$array, $key, $asc = true) { $this->sarray = $array; $this->skey = $key; $this->sasc = $asc; } /** * Sort method * * @access public * @param boolean $remap if true reindex the array to rewrite indexes */ function sortit($remap = true) { $array = &$this->sarray; uksort($array, array(&$this, "_as_cmp")); if ($remap) { $tmp = array(); while (list($id, $data) = each($array)) array_push($tmp, $data); return $tmp; } return $array; } /** * Custom sort function * * @access private * @param mixed $a an array entry * @param mixed $b an array entry */ function _as_cmp($a, $b) { //since uksort will pass here only indexes get real values from our array if (!is_array($a) && !is_array($b)) { $a = $this->sarray[$a][$this->skey]; $b = $this->sarray[$b][$this->skey]; } //if string - use string comparision if (!is_numeric($a) && !is_numeric($b)) { if ($this->sasc) return strcasecmp($a, $b); else return strcasecmp($b, $a); } else { if (intval($a) == intval($b)) return 0; if ($this->sasc) return (intval($a) > intval($b)) ? 1 : -1; else return (intval($a) > intval($b)) ? -1 : 1; } } }
Zapraszam do analizy kodu. Zaproponowana implementacja nie jest oczywiście najdoskonalsza, ale służyć ma jedynie za przykład, że zadanie jest do rozwiązania bez stosowania rekursji w zapytaniach do bazy danych, co jest operacją bardzo kosztowną czasowo.