Drzewo takie zaimplementowałem w moim prywatnym systemie C-Z-W, który niejednokrotnie gościł już na łamach Zyxist.com. Do efektywnej implementacji samego mechanizmu drzewek wykorzystałem algorytm przechodzenia zmodyfikowaną metodą preorder opisany w jednym z artykułów na tej stronie. Mimo wielu zalet, system ma jednak nadal kilka niedociągnięć:
- Problemy z cache'owaniem zawartości - czasem zdarza się, że wyedytuję jakiś obiekt, a na stronie dalej widnieje stara zawartość.
- W niektórych przypadkach podczas renderowania pobierana jest niepotrzebnie ogromna ilość dodatkowych informacji. Wynika to z własności samego drzewa i języka SQL, który nie potrafi w tym wypadku już na poziomie bazy danych odrzucić wszystkich potomków jakiegoś węzła, na którym powinien się zatrzymać.
- Aktualnie ograniczone możliwości personalizacji - dozwolone typy zawartości przechowywanej w obiektach są zakodowane na sztywno.
Aby uzmysłowić sobie skalę problemu, przedstawię fragment metody selectSite(), która pobiera zawartość drzewka przeznaczoną do renderingu:
public function selectSite($hash) { global $sql; // pobierz parametry glownego wezla $result = array(); $sql -> setCache('pagedef_'.$hash, OPD_CACHE_PREPARE); if(ctype_digit($hash)) { $stmt = $sql -> prepare('SELECT d.id, d.left, d.right, t.user_selection, d.language_id, l.title AS `license_title`, l.address AS `license_address`, l.image AS `license_image`, l.important AS `license_important`, dpr.title AS `prev_title`, dpr.full_hash AS `prev_full_hash`, dpr.hash AS `prev_hash`, dne.title AS `next_title`, dne.full_hash AS `next_full_hash`, dne.hash AS `next_hash`, dpa.title AS `parent_title`, dpa.full_hash AS `parent_full_hash`, dpa.hash AS `parent_hash`, dpa.type_id AS `parent_type` FROM ((('.DB_PREFIX.'data d, '.DB_PREFIX.'types t, '.DB_PREFIX.'licenses l) LEFT JOIN '.DB_PREFIX.'data dpr ON dpr.id = d.prev_id) LEFT JOIN '.DB_PREFIX.'data dne ON dne.id = d.next_id) LEFT JOIN '.DB_PREFIX.'data dpa ON (dpa.id = d.parent_id AND dpa.parent_id <> 0) WHERE l.id=d.license_id AND t.id=d.type_id AND d.id=:id'); $stmt -> bindValue(':id', $hash, PDO::PARAM_INT); } else { $stmt = $sql -> prepare('SELECT d.id, d.left, d.right, t.user_selection, d.language_id, l.title AS `license_title`, l.address AS `license_address`, l.image AS `license_image`, l.important AS `license_important`, dpr.title AS `prev_title`, dpr.full_hash AS `prev_full_hash`, dpr.hash AS `prev_hash`, dne.title AS `next_title`, dne.full_hash AS `next_full_hash`, dne.hash AS `next_hash`, dpa.title AS `parent_title`, dpa.full_hash AS `parent_full_hash`, dpa.hash AS `parent_hash`, dpa.type_id AS `parent_type` FROM ((('.DB_PREFIX.'data d, '.DB_PREFIX.'types t, '.DB_PREFIX.'licenses l) LEFT JOIN '.DB_PREFIX.'data dpr ON dpr.id = d.prev_id) LEFT JOIN '.DB_PREFIX.'data dne ON dne.id = d.next_id) LEFT JOIN '.DB_PREFIX.'data dpa ON (dpa.id = d.parent_id AND dpa.parent_id <> 0) WHERE l.id=d.license_id AND t.id=d.type_id AND d.hash=:hash'); $stmt -> bindValue(':hash', $hash, PDO::PARAM_STR); } // itd.
Podane tutaj zapytania pobierają podstawowe informacje o węzłach drzewa. Dalszy algorytm usuwa niewidoczne węzły, a rezultat przekazuje do drugiego zapytania, które pobiera już szczegółowe informacje, jak treść, obrazki, pliki itd. jedynie dla widocznych obiektów. W celu przyspieszenia wyniki są cache'owane.
Jak widać, taki kod jest na dłuższą metę nieco toporny i już od dawna snułem plany na jego przebudowę. Dzisiaj znalazłem interesujące oraz eleganckie rozwiązanie. Polega ono na przypisaniu każdego obiektu do dwóch drzew naraz: globalnego, czyli tego, które jest obecnie, oraz jednego z wielu drzew lokalnych, które funkcjonują niezależnie od siebie. Dla każdego drzewa lokalnego można utworzyć w drugiej tabeli własny rekord z dodatkowymi informacjami... ba, same drzewa można też poukładać w drzewo, choć nie wiem, czy to się już w tym momencie za skomplikowane robić nie będzie. Zalety, jakie daje takie podejście:
- Możemy utworzyć drzewo "szkielet" zawierający wyłącznie strukturę strony. Jeśli istnieje obiekt np. "dział z artykułami", to jest on w nim zawsze liściem.
- Dla artykułów zgromadzonych w pojedynczym dziale tworzone jest nowe drzewo lokalne, działające niezależnie od drzewa szkieletowego.
- Jeśli wyświetlamy fragment szkieletu strony, możemy bez obawy zrezygnować z topornego kodu zamieszczonego powyżej. Wystarczy puścić zapytanie po drzewie szkieletowym i mamy pewność, że pobieranie węzłów zatrzyma się na poziomie działu z artykułami i niżej nie zejdzie.
- A dział z artykułami, po staremu, będzie mógł wykorzystać własny kod pobierania danych z drzewa.
- Jednocześnie żadne z drzew nie będzie wchodzić sobie w paradę, ponieważ nie są ze sobą połączone. Jeśli dodajemy artykuł, wystarczy zaktualizować tylko pola left i right lokalnego drzewa działu artykułów, a reszta pozostaje bez zmian.
- Także system cache jest odciążony. Jeśli dodajemy pojedynczy artykuł, do modyfikacji idzie tylko cache związany z działem artykułów.
Kolejna rzecz, jaką mogłyby rozwiązać takie lokalne drzewka, dotyczy warstwy użytkowej. Każdy obiekt w C-Z-W musi mieć nadany unikalny tzw. "hash", czyli tytuł do wyświetlania w ładnym adresie URL, np. w /o_firmie/historia/czesc2 poszczególne fragmenty odseparowane slashem to hashe poszczególnych obiektów w drzewie zawartości. W drzewie globalnym unikalność musi zachodzić dla absolutnie wszystkich obiektów, czyli nie ma mowy o utworzeniu dwóch subdomen z zawartością i wstawieniu do każdego z nich obiektu home reprezentującego stronę główną. Na razie radziłem sobie w ten sposób, że home działał jak alias do strony głównej ustawionej w danych subdomeny. Nowy mechanizm zezwoli na tworzenie niezależnych drzew szkieletowych dla każdej subdomeny, dając możliwość zapewnienia unikalności na poziomie lokalnym. Nie wspomnę już o takiej rzeczy, jak duplikowanie szkieletu w innej subdomenie. Tworzymy polską wersję językową, wciskamy "Kopiuj", wszystko nam się dokładnie odtwarza w innym miejscu i my już musimy jedynie przetłumaczyć tekst na np. angielski.





