Monthly Archive for 07.2005

Strip #014: Król parkietu

Oto najpilniej strzeżony sekret na świecie, a zarazem wyjaśnienie, dlaczego na parkiecie można mnie zobaczyć dopiero z odpowiednim stężeniem alkoholu we krwi. :)

Tutaj w graficznych przeglądarkach wyświetla się komiks

Czarny humor sponsorowany przez Wyższą Szkołę Informatyki i Zarządzania Copernicus we Wrocławiu, która to nie pozwoliła mi przystąpić do egzaminu poprawkowego z powodu urlopu pani księgowej.

Rekurencja w SQL jest zła

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.

May I inquire about your spoons?

Przeglądałem dziś niemal z łezką w oku stare, najdoskonalsze wytwory fantazji Davida Firtha. To już ponad rok od czasu, kiedy widziałem je pierwszy raz i nadal zachwycają. Psychodeliczny świat, niewiarygodna muzyka i niesamowity klimat stworzony przez osobiście nagrywane przez autora dźwięki i dialogi. Salad Fingers musi zobaczyć każdy.

Strip #013: Lato, czas miłości

Wiosna, lato, gorąco, duszno, podniesiona temperatura sprzyja buzowaniu hormonów. Rodzą się… wyższe uczucia… czy jakoś tak.

Tutaj w graficznych przeglądarkach wyświetla się komiks

Green Marinée

Mania konwertowania szablonów WordPress na Joggera nie ustała. Dziś moją ofiarą padł kolejny, tym razem jest to Green Marinée autorstwa Iana Maina.

Wersja pobieralna znajduje się na moim serwerze.

Do tego obowiązkowy zrzut ekranu, choć poklikać można też na żywo w mojej piaskownicy.

Jak zwykle, szablon jest objęty GPL.

All your page are belong to us

Niewielka sztuczka, którą od zeszłego roku stosuję w PHP, pozwala na łatwe budowanie czytelnych linków. Dzięki temu twój odnośnik zamiast http://example.com/index.php?cat=shop&id=25, może wygląć tak: http://example.com/shop/25, albo nawet tak: http://example.com/shop/25/swieze_jaja_wiejskie.

Oczywiście, to samo można zrobić za pomocą mod_rewrite, ale wada rozwiązania jest taka, że dodanie nowej sekcji do serwisu wymaga ingerencji w pliki serwera WWW.

Mój niewielki hack polega na dodaniu do konfiguracji Apache’a jednej jedynej linijki (dla ułatwienia umieszczamy ją w pliku .htaccess):

ErrorDocument	404	/index.php

Jak można się domyślić, plik index.php jest jedynym plikiem w całym drzewie dokumentów danej domeny i od tego momentu odpowiada on na wszelkie zapytania o nieistniejące dokumenty.

Pozostało jeszcze zatroszczyć się o obsługę tego po stronie kodu:

// sprawdzamy, o jaki dokument jest zapytanie
$request = $_SERVER['REDIRECT_URL'];

// sprawdzamy, czy zapytanie dotyczy nieistniejacego pliku
if (($request) && ($request != '/'))
{
	$request = explode('/', $request);
	$section = $request[1];
	$request = array_splice($request, 2);
	$subsection = implode('/', $request);
}
else // jesli nie, to przekierowujemy na index
{
	header('Location: /index');
	die();
}

// obslugujemy zadanie
switch($section)
{
case 'index':
	// [...]
	break;
case 'shop':
	// [...]
	break;
default:
	// [...] wyswietl blad 404
	break;
}

Zalety - rozwiązanie jest nieinwazyjne względem serwera i pozwala strukturę dokumentów kontrolować na poziomie kodu. Dzięki temu łatwo utrzymywać strukturę, którą ciężko byłoby zarządzać na poziomie mod_rewrite (np. tłumaczone nazwy kategorii).

Wady - nieistniejące strony nie mogą otrzymywać danych przez zlecenia GET i POST, więc wszystkie parametry muszą być częścią URI.

Najciekawsze efekty uzyskuje się przez połączenie tak serwowanych dokumentów ze strukturą statyczną (istniejące pliki nie są przekierowywane). Można zachować wtedy elastyczność rozwiązania z możliwością przekazywania danych z przeglądarki. Dzięki temu możliwa jest obsługa formularzy na stronach.

Keep it dynamic fool!

Coraz częściej mówi się o JS w kontekście zwiększenia użyteczności serwisu, powstają coraz zmyślniejsze metody podłączania naszego kodu do elementów w dokumencie. W idealnej sytuacji plik z kodem HTML nie zawiera ani linijki skryptów, a wszystko odbywa się za pomocą nieinwazyjnego (czyli pozwalającego na pracę ludziom z wyłączonymi skryptami) skryptu zewnętrznego.

Dla lepszego zrozumienia proponuję przeanalizować metodę działania prostego skryptu do podświetlania obrazków. Ma on nad czystym CSS taką przewagę, że pozwala również na podświetlanie dynamicznie wstawianych obrazków.

Szczytem techniki jest jednak według mnie Behaviour - skrypt łączący strukturę CSS z mocą JS. Z jego pomocą możemy z łatwością utworzyć arkusz skryptów w zapisie bardzo podobnym do arkusza styli i podłączyć go automatycznie do swojej witryny.

Największą popularnością cieszą się z kolei ostatnio stosunkowo nowe skrypty spod znaku script.aculo.us. I nic dziwnego, skoro pozwalają one na bajecznie łatwą implementację dowolnie rozbudowanych efektów, a dodatkowo umożliwiają bardzo łatwe dodanie do serwisu funkcjonalności drag’n'drop. Pięć minut pracy i przykład można zobaczyć w moim laboratorium.

Powstaje tylko pytanie - czy jeśli da się coś zrobić łatwo oznacza, że powinniśmy tego używać? Oczywiście, że nie. Takie rozwiązania mają bardzo wąskie pole zastosowania, niewiele jest bowiem stron, które nie tylko coś zyskają, ale dodatkowo nie ucierpią od takich atrakcji. Pamiętajmy, że zawsze najważniejszy jest fallback do wersji czysto statycznej.