JustPaste.it

PHP Data Objects - nowy wymiar baz danych

PHP Data Objects to nowe wbudowane rozszerzenie PHP, które sprawi, że stare metody komunikacji z bazą danych odchodzą do historii. Pozwala ono na obsługę różnych serwerów bazodanowych przy pomocy jednolitego interfejsu. Naucz się z niego korzystać!

PHP jest bardzo dobrym językiem, lecz do tej pory ciągnęło za sobą szereg anachronizmów i udziwnień powstałych jeszcze w pionierskich latach PHP/FI. Dopiero niedawno twórcy zaczęli je stopniowo usuwać. Począwszy od PHP 5.1 rewolucja objęła także sposoby komunikowania się z bazami danych. Niejeden z nas klął, na czym świat stoi, gdy musiał przepisywać od nowa całą aplikację, ponieważ do łączenia się z nową bazą potrzebne są zupełnie inne i nie zawsze kompatybilne funkcje. Przypatrzmy się przykładom. Oto kod, który pobiera z bazy MySQL listę wszystkich klientów w hipotetycznym serwisie WWW:

<?php
 
mysql_connect('localhost', 'root', '');
mysql_select_db('moja_kurde_baza');
 
$result = mysql_query('SELECT * FROM klienci');
 
while($row = mysql_fetch_assoc($result))
{
print_r($row);
echo '<br>';
}
 
mysql_close();
?>

Analogiczny przykład dla bazy SQLite wygląda następująco:

<?php
$db = sqlite_open('./sklep.sqlite');
 
$result = sqlite_query($db, 'SELECT * FROM klienci');
 
while($row = sqlite_fetch_array($result, SQLITE_ASSOC))
{
print_r($row);
echo '<br>';
}
 
sqlite_close($db);
?>

Nie ma w tym żadnej logiki. Każde rozszerzenie do każdej bazy wymaga innej kolejności podawania identycznych parametrów, innego sposobu pobierania wyników, a moje osobiste doświadczenia pokazują, że stwarza to naprawdę dużo problemów. Programiści na własną rękę podejmowali próby rozwiązania problemu, pisząc w PHP odpowiednie nakładki, zwane sterownikami baz danych albo warstwami baz danych (database layers). Udostępniały one jednolity, obiektowy interfejs, lecz były napisane w PHP i przez to nie zawsze dostatecznie wydajne. Takimi bibliotekami są np. Creole i AdoDB.

Po wydaniu PHP 5.0.0 rozpoczęły się jednak prace nad zunifikowanym rozszerzeniem obsługi baz danych zwanym PHP Data Objects. Wersje beta można było ręcznie doinstalować już do PHP 5.0 za cenę utraty niektórych możliwości oraz faktu, że po wydaniu PHP 5.1 twój kod przestanie działać. Jednak PHP 5.1 już istnieje, a wraz z nim stabilna wersja PDO, która w dodatku jest "firmowo" wbudowana w pakiet i nie trzeba się zbytnio wysilać, aby zacząć jej używać.

PDO składa się z dwóch zasadniczych części: zunifikowanego interfejsu oraz sterowników poszczególnych baz danych, które implementują dostarczane przezeń metody. Twórcy dołożyli wszelkich starań, aby odseparować Cię od kwestii technicznych związanych z komunikacją z każdą dostępną bazą. PDO potrafi samodzielnie emulować niektóre z oferowanych możliwości, jeżeli któraś baza ich nie wspiera, a także sprawia, że bez względu na jej rodzaj nie będziesz musiał przepisywać ani jednej linijki kodu w przypadku przesiadki na inną (oczywiście nie wspominam tu o zapytaniach SQL, ale to sprawa innego kalibru). Jednak pamiętaj: PDO nie jest warstwą bazy danych, lecz zunifikowanym interfejsem. Wysokopoziomowe możliwości teoretycznie powinno się dodawać samodzielnie, lecz i na to jest rada. Niedawno powstała polska biblioteka Open Power Driver o identycznym API, jak PDO (tj. ich użycie jest dokładnie takie same) oferująca brakujące elementy, m.in. cache'owanie i konsolę debugową. Napisana jest w PHP 5.1.

Zacznijmy zatem od zainstalowania PDO. Jeżeli żyjesz w świecie Linuksa/Uniksa i masz już PHP 5.1.1, nie musisz robić praktycznie nic, poza dokompilowaniem sterownika do twej wymarzonej bazy (samo PDO jest dostępne od razu). Oto przykład:

./configure --with-pdo-mysql=/usr/local/mysql/bin/mysql_config
make
make install

Sterownik do bazy danych SQLite instalowany jest automatycznie. Do innych serwerów DB odpowiednią dyrektywę należy sprawdzić w dokumentacji PHP. W systemie Windows sprawa jest nieco prostsza. Trzeba otworzyć php.ini i dodać do niego takie linijki pod listą rozszerzeń:

extension=php_pdo.dll
extension=php_pdo_mysql.dll

Zrestartuj serwer WWW i już wszystko jest gotowe. Przystąpmy zatem do dzieła.

Pierwsze połączenie

Jako że PHP zrywa powoli ze swoją czysto proceduralną przeszłością, nowe rozszerzenia oferują nam obiektowe interfejsy. Podobnie jest w przypadku PDO. Połączenie polega na utworzeniu odpowiedniego obiektu, który sam się zwolni i je zakończy na samym końcu skryptu dzięki destruktorom. Ewentualne błędy raportowane są jako wyjątki, które powinniśmy przechwycić.

<?php
 
try
{
$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
echo 'Połączenie nawiązane!';
}
catch(PDOException $e)
{
echo 'Połączenie nie mogło zostać utworzone: ' . $e->getMessage();
}
?>

Parametr pierwszy przekazywany do konstruktora klasy PDO to tzw. DSN (database source). Określamy w nim nazwę sterownika, który chcemy wykorzystać, host, pod jakim pracuje baza oraz nazwę takowej, z którą chcemy się połączyć. Dwa następne to użytkownik i jego hasło użyty do nawiązania połączenia. Nie wszystkie sterowniki obsługują coś takiego, dlatego podawanie ich zależy od tego, z jaką bazą się łączymy. To w zasadzie wszystko. W przypadku błędu połączenia konstruktor wygeneruje wyjątek przechwytywany odpowiednią klauzulą try...catch.

Zanim zaczniemy dalszą zabawę, udostępnię jeszcze testową tabelę dla MySQL'a 4.1, na której będę wszystko demonstrować:

CREATE TABLE `products` (
`id` int(8) NOT NULL AUTO_INCREMENT,
`name` varchar(32) collate latin1_general_ci NOT NULL DEFAULT '',
`description` varchar(255) collate latin1_general_ci NOT NULL DEFAULT '',
`category` int(8) NOT NULL DEFAULT '0',
`counter` int(8) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `category` (`category`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
 
INSERT INTO `products` VALUES (1, 'Apples', 'Natural and delicious apples from southern Poland, only for 4,99 zl per kg!', 1, 3);
INSERT INTO `products` VALUES (2, 'Pears', 'Pears from mr Gajewski''s farm near Zielona Gora, always fresh. Only for 4,30 zl per kg.', 1, 3);
INSERT INTO `products` VALUES (3, 'Mineral water', 'Very good regular mineral water from Tatras.', 2, 3);
INSERT INTO `products` VALUES (4, 'Apple juice', 'Apple juice from Polish fruits. Certified with ISO 9001.', 2, 3);

Wybaczcie za język angielski w rekordach, ale akurat taka tabelka mi wpadła w ręce, gdy poszukiwałem obiektu do testów :).

Pobieranie danych

Pobieranie danych zwróconych z zapytania SELECT jest niezwykle proste. PDO korzysta z iteratorów, dlatego nasz kod sprowadza się do stworzenia zwykłej pętli foreach:

<?php
try{

$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

foreach($pdo -> query('SELECT id, name, description FROM products') as $row)
{
echo '<p>'.$row['id'].': <b>'.$row['name'].'</b> '.$row['description'].'</p>';
}
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

Druga linijka po konstruktorze (setAttribute()) informuje PDO, że dalsze błędy bazy i zapytania także powinny być zwracane jako wyjątki. Możliwy jest tutaj również tryb pracy cichej (PDO::ERRMODE_SILENT), w którym sami musimy się pofatygować o obsługę komunikatu, jak za starych lat, oraz PDO::ERRMODE_WARNING generujący standardowe ostrzeżenia PHP.

Przyjrzyjmy się teraz samej pętli. Pobiera ona z wyniku zwróconego przez metodę query() poszczególne rekordy w postaci tablic asocjacyjnych. Co jednak będzie, kiedy zechemy coś innego? Nic takiego; przecież pisałem, że działają tutaj iteratory. Wystarczy przenieść metodę query() nieco wyżej i na zwróconym zbiorze wyników wywołać metodę setFetchMode():

<?php
try{

$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$stmt = $pdo -> query('SELECT id, name, description FROM products');
$stmt -> setFetchMode(PDO::FETCH_NUM);
 
foreach($stmt as $row)
{
echo '<p>'.$row[0].': <b>'.$row[1].'</b> '.$row[2].'</p>';
}
$stmt -> closeCursor();
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

Jeżeli z jakiegoś powodu z iteratorów korzystać nie chcemy, zawsze możemy jawnie wywołać metodę fetch() i w niej dokonać ustawienia typu zwracanych rekordów:

<?php
 
try{

$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$stmt = $pdo -> query('SELECT id, name, description FROM products');

while($row = $stmt -> fetch(PDO::FETCH_NUM))
{
echo '<p>'.$row[0].': <b>'.$row[1].'</b> '.$row[2].'</p>';
}
$stmt -> closeCursor();
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

Dane pobierane są z bazy w PDO za pomocą tzw. kursorów. Odpowiednio skonfigurowane mogą oddać wiele przysług, np. zezwalając na poruszanie się także do tyłu. Niemniej jednak zawsze, nawet w tak prozaicznych przykładach, jak powyższe, po zakończeniu pobierania danych niezbędne jest ich zamknięcie, aby móc wykonać kolejne zapytania! Odpowiada za to metoda closeCursor(), którą musimy wtedy wywołać. Zauważ więc, że w trakcie pobierania wyników jednego zapytania, nie będziesz w stanie wykonać nic innego! I w dodatku ja nie uważam tego za wadę, a za zaletę. Popatrz: ileż to widziałeś mocożernych skryptów, które potrafią wykonać po sto zapytań, aby wygenerować jedną podstronę? Wszystko przez to, iż część z nich jest wywoływana w pętlach. Kursory wymuszają lepsze poznanie języka SQL i taką organizację całości, aby wszystko było zgodne z wymogami, a przy tym wydajniejsze.

Aktualizacja rekordów

Jeżeli dotychczas korzystałeś ze standardowej biblioteki komunikacji np. z MySQL'em, będziesz musiał odzwyczaić się od istnienia jednej, uniwersalnej funkcji do wysyłania zapytań. PDO ma ich kilka różniących się końcowym przeznaczeniem. Poznaliśmy już query() generującą zbiór wyników. Do wysyłania zapytań typu UPDATE czy DELETE, stosuje się exec(), która automatycznie zwraca ilość zmodyfikowanych rekordów, a nie jakieś wzięte z kosmosu true i false :). Posługiwanie się nią jest bardzo intuicyjne:

<?php
 
try{

$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

echo 'Zaktualizowanych rekordów: '.$pdo -> exec('UPDATE products SET counter = (counter + 1)');
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

Przykład ten wyświetli, ile rekordów zostało zmodyfikowanych w wyniku działania zapytania UPDATE.

Podpinanie parametrów

Idea ta spotykana w nowych sterownikach baz danych nie ma jeszcze swej polskiej nazwy. Po angielsku mówi się binding parameters i stąd też niektórzy już uknuli kolejny idiotyczny polskawy termin bindowanie parametrów. Jako że osobiście mam awersję do tego typu kaleczenia rdzennego języka, proponuję nieco inną nazwę, zwłaszcza że znalezienie jej jest wręcz banalne. "To bind" znaczy "wiązać, przywiązywać", ale jako że się niezbyt to komponuje, lepszy byłby termin "podpinania parametrów" i tego też będę się konsekwentnie trzymał nie tylko w tym tekście, ale i wszystkich innych, jakie powstaną w przyszłości.

Ogólnie rzecz biorąc wykonywanie zapytania składa się tutaj z trzech etapów. Pierwszy to jego przygotowanie, podczas którego serwer wstępnie przetwarza je, by zaoszczędzić czas. Zostawiamy w nim odpowiednie luki, do których odpowiednimi metodami podpinamy rozmaite wartości. PDO samodzielnie zajmuje się w tym wypadku ochroną danych przed SQL Injection. Kiedy wszystko jest na swoim miejscu, nakazujemy wykonać zapytanie. Kroki drugi i trzeci możemy powtarzać kilkakrotnie i całość wykonana zostanie szybciej, niż w przypadku oddzielnego wysyłania zapytań, gdyż (jak wspomniałem) serwer zajmuje się tym na wstępie i przy następnych żądaniach korzysta z tego, co już było. Oczywiście niezbędnym warunkiem jest tu, aby obsługiwał on samodzielnie ten proces i nie zmuszał PDO do jego emulowania.

Oto, jak to wygląda w praktyce:

<?php
try
{
$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$cat = 1;
$stmt = $pdo -> prepare('SELECT id, name, description FROM products WHERE category = :category');
$stmt -> bindValue(':category', $cat, PDO::PARAM_INT);
$stmt -> execute();
 
while($row = $stmt -> fetch(PDO::FETCH_ASSOC))
{
echo '<p>'.$row['id'].': <b>'.$row['name'].'</b> '.$row['description'].'</p>';
}
$stmt -> closeCursor();
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

Najpierw metodą prepare() przygotowujemy zapytanie i otrzymujemy obiekt PDOStatement umieszczony w zmiennej $stmt. Luka na dane w naszym przypadku to :category. Do niej metodą bindValue() przypisujemy odpowiednią wartość. Przy tej okazji możemy zwrócić PDO uwagę na typ danych, jakie chcemy zamieścić, dzięki czemu dokona on niezbędnych przekształceń pod tym kątem. Tu także następuje eliminowanie ewentualnych dziur a'la SQL Injection, na które narażone są aplikacje z "tradycyjną" obsługą zapytań. Po podpięciu wszystkiego nakazujemy wykonać całość metodą execute() udostępnioną przez PDOStatement. W przypadku zapytań SELECT nie zwraca ona nic, gdyż wszystko przypisywane jest do już istniejącej $stmt (taka oszczędność).

Metoda bindValue() podpina do zapytania jedynie wartość, ale możliwe jest również podpinanie zmiennych z użyciem mechanizmu referencji. Wystarczy wtedy użyć bindParam(). Ilustruje to poniższy przykład:

<?php
 
try{

$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$cat = 1;
$stmt = $pdo -> prepare('SELECT id, name, description FROM products WHERE category = :category');
$stmt -> bindParam(':category', $cat, PDO::PARAM_INT);
$stmt -> execute();
 
while($row = $stmt -> fetch(PDO::FETCH_ASSOC))
{
echo '<p>'.$row['id'].': <b>'.$row['name'].'</b> '.$row['description'].'</p>';
}

$cat = 2;
$stmt -> execute();
 
while($row = $stmt -> fetch(PDO::FETCH_ASSOC))
{
echo '<p>'.$row['id'].': <b>'.$row['name'].'</b> '.$row['description'].'</p>';
}
$stmt -> closeCursor();
}
catch(PDOException $e)
{
echo 'Połączenie nie mogło zostać utworzone: ' . $e->getMessage();
}
?>

Zauważ, że bindParam() podpięło nam nie wartość zmiennej $cat z ID kategorii do pokazania, ale samą zmienną. Dzięki temu przy ponownym wysyłaniu podobnego zapytania ewentualna modyfikacja zwracanej kategorii odbywa się poprzez zmianę wartości tej zmiennej. Przykład ten pokazuje siłę podpinania. Nasze zapytanie jest wstępnie przetwarzane w metodzie prepare(), a dalej MySQL korzysta z jego wyników, by znacznie przyspieszyć cały proces. Próbowałem zmierzyć, jak się ma prędkość "tradycyjnego" sposobu do podpinania przy wysyłaniu jednego zapytania, ale Apache Bench zachowywał się dość dziwnie i ze zwracanych informacji nie dało się nic wyczytać. Dlatego przyjmijmy, iż wydajność obu sposobów jest porównywalna przy większych możliwościach oferowanych przez ten drugi.

PDO::FETCH_CLASS

Teraz chciałbym zaprezentować pewną właściwość oferowaną przez PDO, która spodoba się z pewnością osobom, które zadają sobie trud stworzenia DAO (Database Abstraction Objects), czyli specjalnego zbioru klas separującego programistę poszczególnych modułów aplikacji (logowania, wyświetlania listy newsów itd.) od wywoływania zapytań. Wszystkie dane pobierane są niejawnie odpowiednimi metodami. Dotychczasowe obecne w PHP sterowniki dla różnych serwerów oferowały wprawdzie zwracanie rekordów jako anonimowe obiekty (FETCH_OBJ), lecz w praktyce posługiwali się tym tylko puryści, bo czym się taki anonimowy obiekt bez metod różni od tablicy?

Twórcy PDO poszli po rozum do głowy, ale zamiast niego dostali PDO::FETCH_CLASS. Rozumu mieli trochę własnego i oczywiście bez wahania wstawili otrzymany prezencik do PDO. I chwała im za to, ponieważ korzystając z tego sposobu zwracania rekordów, możemy określić, jakiej klasy obiekty z danymi chcemy tworzyć i ew. przekazać ich konstruktom odpowiednie parametry. Popatrzmy na to:

<?php
 
class myDAO
{
protected $nameConvert;

public function __construct($nameConvert)
{
$this -> nameConvert = $nameConvert;
} // end __construct();

public function convert()
{
if($this -> nameConvert)
{
$this -> name = strtoupper($this -> name);
}
} // end convert();

}
 
try{

$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo -> prepare('SELECT id, name, description FROM products WHERE category = :category');
$stmt -> bindValue(':category', 1, PDO::PARAM_INT);
$stmt -> execute();
$stmt -> setFetchMode(PDO::FETCH_CLASS, 'myDAO', array(0 => false));
 
while($row = $stmt -> fetch())
{
echo '$row jest obiektem klasy '.get_class($row).'<br/>';
$row -> convert();
echo '<p>'.$row -> id.': <b>'.$row -> name.'</b> '.$row -> description.'</p>';
}
$stmt -> closeCursor();
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

W przeciwieństwie do innych formatów, ten musimy koniecznie ustawić przy pomocy metody setFetchMode(). Za drugi i trzeci parametr podajemy kolejno: nazwę klasy oraz tablicę z parametrami dla konstruktora. UWAGA: dokumentacja do wersji 5.1.1 nic o tym nie mówi, dopiero wczytanie się w komentarze użytkowników pozwoliło mi dowiedzieć się o takich właściwościach. Zatem przyjrzyjmy się działaniu skryptu.

W momencie wywołania metody fetch() tworzony jest obiekt żądanej przez nas klasy (w tym wypadku jest to "myDAO"), a biblioteka tworzy w nim nowe, publiczne pola odpowiadające tym istniejącym w zwróconym rekordzie. Kiedy wszystko jest gotowe, odpalany jest konstruktor (pamiętaj o tej kolejności!) i całość jest nam udostępniana. Tak zwracane przez PDO rekordy w postaci obiektów mogą mieć własne metody, dodatkowe pola itd., a my możemy sterować całym tym procesem. Co powiesz np. na przechwycenie samego umieszczania danych w obiekcie i namieszania w nim? Jest to możliwe, wszak mamy metodę magiczną __set():

<?php
 
class myDAO
{
public function __set($name, $value)
{
echo '<p><b>Ustawianie `'.$name.'` na `'.$value.'`</b></p>';

} // end __set();
 
}
 
try{
 
$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
$stmt = $pdo -> query('SELECT id, name, description FROM products');
$stmt -> setFetchMode(PDO::FETCH_CLASS, 'myDAO');
while($row = $stmt -> fetch())
{
echo '$row jest obiektem klasy '.get_class($row).'<br/>';
}
$stmt -> closeCursor();
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

W tym przykładzie klasa myDAO istnieje tylko po to, abyśmy mogli podpiąć się pod proces zwracania rekordu. PDO myśli, że tworzy w nim nowe pola, ale jako że te nie są zadeklarowane, wszystko to wyłapuje metoda magiczna set() i teraz do akcji wkraczamy my. Mamy pełne pole do popisu. Tutaj zdecydowałem się po prostu na wyświetlanie każdego z pól i w efekcie zwrócony obiekt w $row nie niesie ze sobą dosłownie nic :). Jednak to już tylko moja sprawa - tak to sobie oprogramowałem i tak to działa.

Jakie to może mieć zastosowanie w DAO? Osobiście pracuję już nad rozwiązaniem, w którym używam specjalnej klasy do rozbijania na mniejsze jednostki zbiorczego wyniku z danymi pochodzącymi z kilku tabel. Po prostu podpiąłem się pod metodę set() i sprawdzam: jeżeli nazwa pola ma prefiks AAA, to wrzuć to do podobiektu z danymi rodzaju AAA, jeżeli BBB, to BBB itd. Dzięki temu panuje porządek, a jako projektant nie mam zarwania głowy z ręcznym rozbijaniem takiego wyniku na obiekty. Po prostu moja magiczna klasa robi wszystko automatycznie i po cichu już w momencie, gdy pracuje PDO. Jak duży ma to wpływ na wydajność, chyba mówić nie muszę?

Dlaczego nie da się policzyć ilości rekordów z SELECT?

Osoby nowe w PDO często zadają takie pytanie. Chcą sobie kulturalnie policzyć wcześniej, ile rekordów zwrócił im SELECT i jest problem. W dokumentacji nie ma o tym ani słowa. To nie pomyłka. Pora wyjaśnić pewną ważną rzecz... Święty Mikołaj nie istnie... ups... nie ten tekst. No, ale morał jest podobny. Tak naprawdę oficjalne biblioteki do komunikacji z bazą danych nie mają czegoś takiego, jak wstępne liczenie ilości rekordów, gdyż sama baza nie wie jeszcze wtedy, ile ich ostatecznie zwróci. Cel w tym jest jeden: wydajność. Dzięki temu PHP może zacząć pobieranie wyników już po zbudowaniu pierwszego rekordu, nie bacząc na to, że reszta nie jest gotowa. Jak więc zatem robiły to stare rozszerzenia? Zwyczajnie oszukiwały, ściągając samodzielnie wszystkie rekordy, zapisując je w pamięci i udostępniając ze swojego prywatnego bufora. Było to dobre dla małych porcji, ale wystarczyło już spróbować pobrać 1 megabajt danych, aby naocznie przekonać się, że droga na skróty nie popłaca. PHP marnował czas na ściąganie tego wszystkiego, pamięć na składowanie, później znowu czas na udostępnianie skryptowi. Wprawdzie ostatnio pojawiły się dodatkowe funkcje pozwalające to omijać, lecz mało kto o nich wiedział. Teraz nie ma "zlituj". Musisz nauczyć się tak pisać skrypty, aby dodatki na wzór mysql_num_rows() nie były Ci w ogóle potrzebne.

Pozornie problem rozwiązuje wywołanie wcześniej zapytania SELECT COUNT, które policzyłoby nam wszystko, ale kryje się tu pewna pułapka. Jeżeli na ten czas nie zablokujemy bazy, może się zdarzyć, że między zliczaniem, a pobieraniem rekordów ktoś nam jakiś doda/skasuje i w efekcie informacja będzie nieadekwatna do tego, co baza zacznie zwracać. Można jednak zablokować na ten czas tabelę do odczytu, lecz należy samodzielnie sprawdzić, jak takie rozwiązanie będzie zachowywać się na twojej bazie.

Powyżej opisany sposób pobierania kolejnych rekordów z bazy tłumaczy także wspomnianą wcześniej konieczność zamykania kursora i niemożność wykonywania w jego trakcie innych zapytań. W starszych bibliotekach nie było to konieczne ze względu na czynione przez nie oszustwa - wydawało się, że zapytania wywołujemy rekurencyjnie, lecz tak naprawdę ich wyniki były już wtedy dawno pobrane.

Atrybuty połączenia

Ostatnią rzeczą, o której chciałbym wspomnieć, są atrybuty połączenia. Spotkałeś się z nimi w poprzednich przykładach przy okazji informowania PDO, że wszystkie błędy mają być raportowane jako wyjątki (metoda setAttribute()). Mamy też getAttribute(), którym możemy pobrać informacje o różnych aspektach pracy bazy. Poniższy przykład pokazuje nam kilka przykładowych parametrów połączenia:

<?php
try{
$pdo = new PDO('mysql:host=localhost;dbname=moja_kurde_baza', 'root', 'root');
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
echo '<p>Wersja bazy danych: '.$pdo -> getAttribute(PDO::ATTR_SERVER_VERSION).'</p>';
echo '<p>Wersja biblioteki klienckiej: '.$pdo -> getAttribute(PDO::ATTR_CLIENT_VERSION).'</p>';
echo '<p>Używany sterownik: '.$pdo -> getAttribute(PDO::ATTR_DRIVER_NAME).'</p>';
}
catch(PDOException $e)
{
echo 'Błąd bazy danych: ' . $e->getMessage();
}
?>

Więcej parametrów znajduje się w dokumentacji.

Open Power Driver

Open Power Driver jest napisaną w PHP 5.1 biblioteką, która dodaje do PDO niektóre brakujące możliwości, m.in. cache'owanie wyników zapytań. Poza tym posiada identyczny interfejs, jak pierwowzór, stąd też wszystkie tematy omówione w tym artykule będzie realizować się z pomocą OPD dokładnie w ten sam sposób. Jedyną różnicą jest nieco inna inicjacja oraz odpowiednie metody umożliwiające cache'owanie.

Aby zainstalować OPD, pobierz najnowszą wersję ze strony www.openpb.net. W archiwum znajdź katalog "lib" i skopiuj pliki z niego do struktury katalogowej twojego projektu. Następnie utwórz katalog cache, który będzie wykorzystywany do przechowywania scache'owanych wyników. Nie zapomnij nadać skryptom praw zapisu do niego! Przejdźmy teraz do pisania samego skryptu:

<?php
define('OPD_DIR', '../lib/');
require(OPD_DIR.'opd.class.php'); // 1
 
try
{
$pdo = opdClass::create(array(
'dsn' => 'mysql:host=localhost;dbname=moja_kurde_baza',
'user' => 'root',
'password' => 'root',
'cache' => './cache/',
'debugConsole' => true
)); // 2
 
$pdo -> setCacheExpire(30, 'my_products'); // 3
foreach($pdo -> query('SELECT id, name, description FROM products') as $row)
{
echo '<p>'.$row['id'].': <b>'.$row['name'].'</b> '.$row['description'].'</p>';
}
}
catch(PDOException $e)
{
opdErrorHandler($e); // 4
}
?>

Na przykładzie oznaczone są różnice. Na początek musimy oczywiście załączyć naszą nakładkę. Należy bezwzględnie umieścić przedtem ścieżkę do plików biblioteki w stałej OPD_DIR (1). Na potrzeby inicjacji mamy specjalną fabrykę, do której wrzucamy albo tablicę z konfiguracją albo ścieżkę do pliku INI zawierającą takową. Dodatkowe dyrektywy to cache wskazująca na katalog cache oraz debugConsole włączająca konsolę debugową. Z konsoli można dowiedzieć się wielu przydatnych informacji: jakie zapytania zostały wykonane, ile rekordów im uległo oraz czas ich wykonywania (2). Bibliotekę można także zainicjować w taki sam sposób, jak w PDO. Trzeba tylko pamiętać, by wywołać konstruktor klasy opdClass, a nie PDO, po czym ręcznie wprowadzić brakujące parametry. OPD ustawia automatycznie tryb raportowania błędów jako wyjątki i nie musimy tego robić ręcznie.

Jeżeli przed zapytaniem umieścimy taką metodę, jego wyniki zostaną scache'owane. W tym przykładzie pragniemy zapisać je pod nazwą my_products na 30 sekund. Przez ten czas każdy, kto wejdzie na stronę, otrzyma wyniki właśnie z bufora cache; zapytanie nie zostanie fizycznie wykonane, co zwiększa wydajność. Istnieje także możliwość cache'owania wiecznego i ręcznego usuwania jego plików, co ma zastosowanie w przypadku rzadko odświeżanych danych. Pozostała obsługa OPD jest dokładnie taka sama, jak PDO (3).

OPD udostępnia nam także firmowy handler wyjątków: opdErrorHandler(), który automatycznie formatuje komunikat błędu.

Największą zaletą biblioteki jest oczywiście cache'owanie zapytań. Oprócz podanej wyżej metody, można zastosować także setCache(), która tworzy wieczny cache. Jeżeli dane się zmienią, musimy usunąć go ręcznie metodą clearCache(). Przydaje się to przy rzadko aktualizowanych zbiorach danych, dla których wybieranie czasu np. 30 sekund byłoby tylko marnotrawstwem mocy - pamiętaj, że aby tak krótki czas rzeczywiście się opłacał, twoja witryna musi mieć oglądalność rzędu kilkudziesięciu osób na sekundę. Teoretycznie pomogłoby ustawienie dłuższych limitów czasu, lecz to mogłoby doprowadzić do pewnych przekłamań: ty już dodałeś nowy rekord, ale internauci jeszcze go nie widzą, bo plik cache nie stracił ważności. Open Power Driver daje także możliwość cache'owania zapytań korzystających z podpinania (prepare()... execute()) - w tym wypadku musimy ustawić właściwosci cache dla każdej odpalonej metody execute() przed wywołaniem prepare().

OPD wprowadza także kilka "przyspieszaczy" pisania, które skutecznie skracają kod. Przykładowo, jeżeli chciałbyś wykonać zapytanie UPDATE i przekazać do niego jakiś parametr za pom. podpinania, w "oryginale" jesteś zmuszony do zabawy z całym arsenałem prepare() itd. OPD pozwala na przekazanie do metody exec() dodatkowego argumentu - jeśli go podamy, zostanie on automatycznie podpięty do zapytania jako :id, np.

$pdo -> exec('UPDATE `produkty` SET `ilosc` = (`ilosc` + 1)
WHERE id = :id'
, $_GET['id']);

Pozostałe możliwości biblioteki opisane są w załączonej do projektu anglojęzycznej dokumentacji i tam też odsyłam zainteresowanych.

Zakończenie

Czy PDO się przyjmie? Wszystko wskazuje na to, że tak. Już samo pobieżne przewertowanie for dyskusyjnych pozwala sądzić, że przedsięwzięcie wzbudziło niemałe zainteresowanie i ewentualny brak rozbudowanej bazy materiałów można tłumaczyć jedynie czasem obecności stabilnej wersji biblioteki w Internecie. PHP 5.1.0 wydane zostało 20-go listopada, zatem w momencie publikacji tego tekstu nie minął od tamtego czasu jeszcze miesiąc. Pozostaje mieć nadzieję, iż autorzy tutoriali i artykułów staną na wysokości zadania i stare, niekompatybilne ze sobą sterowniki, odejdą do historii, a kombajny typu AdoDB będą musiały przeorganizować swoją strukturę. Jest to dobrze i dla nas, ponieważ z pewnością ulegną one pewnemu odchudzeniu, a mniej kodu => szybsza kompilacja => większa wydajność. Dziękujmy twórcom PHP za wspaniałą robotę.

Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com

 

Źródło: http://webcity.pl/webcity/pdo_nowy_wymiar_baz_danych

Licencja: Creative Commons - użycie niekomercyjne - bez utworów zależnych