Autor: Tomasz Jędrzejewski
Data publikacji: 28.10.2006, 08:15 | Ostatnia modyfikacja: 02.11.2006, 18:21
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.
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 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.
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.
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.
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Ä™?
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.
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 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.
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
Waszym zdaniem:
__note :: 14.02.2008, 00:56 :: #98
Fantastyczny tekst!