Większość stron wykorzystuje bazy. Pisanie za każdym razem wielu funkcji do obsługi bazy danych mija się z celem. Lepszym rozwiązaniem jest klasa do obsługi bazy danych MySql. Dzisiaj opiszę taką właśnie klasę, która na pewno ułatwi każdemu programowanie w PHP.
Moja klasa, jak większość klas, posiada swoje właściwości. Podstawowe właściwości to:
- połączenie
- zapytanie, które jest wykonywane
- błąd, który został wygenerowany
- liczba rekordów
- id wstawionego rekordu
Można tutaj dodać właściwość odpowiadającą za katalog w którym trzymany będzie kesz. jednak wg mnie lepszym rozwiązaniem jest trzymanie takiej informacji w pliku z ustawieniami połączenia z bazą danych (host, nazwa bazy danych, użytkownik, hasło, prefiks tabel). Chodzi tutaj głównie o łatwiejsze konfigurowanie projektu.
class baza { private $polaczenie=0; public $zapytanie=''; public $error=''; public $liczbaRekordow=0; public $idRekordu=0; }
Teraz zastanówmy się, co jest niezbędne w takiej klasie:
- konstruktor
- destruktor
- keszowanie zapytań
- obsługa połączenia
- pobieranie wyników po wykonaniu zapytania i automatyczne zwalnianie zasobów
- obsługa transakcji
W tym wypadku konstruktor klasy jest potrzebny, żeby włączyć do skryptu plik z ustawieniami bazy danych. Równie dobrze ustawienia te można pobierać przed samym połączeniem z bazą, ale w takim wypadku trzeba zaimportować informację o katalogu w którym będzie trzymany kesz lub stworzyć właściwość, która będzie odpowiedzialna za informacje o tym katalogu. Jeśli zdecydujemy się na to drugie rozwiązanie, to będziemy musieli pamiętać, żeby przypisać odpowiednią ścieżkę przy każdym utworzeniu klasy.
public function __construct() { require_once('inc/ustawieniaBazy.inc.php'); }
Kolejną ważną metodą będzie destruktor. Będzie on odpowiedzialny za zwalnianie połączenia z bazą.
public function __destruct() { $this->polaczenie && $this->disconnect(); }
Klasa powinna być napisana z myślą nie tylko o zautomatyzowaniu pracy i ułatwieniu pisania, ale również o oszczędności zasobów serwera. Jednym z takich elementów oszczędzających zasoby będzie keszowanie zapytań.
Bazy danych zapisują dane w plikach. Każde wykonanie zapytania, to wczytanie i analiza wielu plików. Skoro tak, to o wiele oszczędniejsze i szybsze będzie pobranie wygenerowanego wcześniej zapytania z jednego pliku (kesz). Przy dodawaniu mechanizmu keszowania do klasy warto pamiętać o tym, że w każdym systemie będą zapytania, które nie muszą, a wręcz nawet nie powinny być keszowane. Po co keszować zapytanie, które i tak musi być za każdym razem wykonane.
Dla przykładu użytkownik X ma pewne prawa i właśnie korzysta z systemu. Nagle administrator postanawia zabrać mu dostęp do niektórych elementów systemu. Jeśli nie sprawdzimy czy były jakieś zmiany w prawach dla użytkownika, to skąd system ma wiedzieć, że od tego momentu użytkownik ma zmienione prawa (np zabroniony dostęp do całego panelu administracyjnego)?
Jeszcze jedna ważna sprawa związana z keszowaniem. Każde zapisane zapytanie będzie w pliku, którego nazwą będzie zapytanie. Jednak bywa, że zapytania są bardzo długie. Tworzenie plików z tak długimi nazwami mija się z celem. Dla skrócenia nazwy można stosować funkcje szyfrujące np md5. To da nam unikalne i niezbyt długie nazwy plików.
if($keszujZapytanie) { $keszujZapytanie=md5($this->zapytanie); if(file_exists(KESZ.$keszujZapytanie)) { $this->wyniki=unserialize(file_get_contents(KESZ.$keszujZapytanie)); $this->liczbaRekordow=count($this->wyniki); return true; } }
Połączenie możemy nawiązywać przy tworzeniu klasy (w konstruktorze). Każde nawiązanie połączenia, wybranie bazy danych, ustawienie kodowania itd, zabiera określoną ilość czasu, zużycia procesora, pamięci itd. Jeśli strona będzie oglądana raz na jakiś czas, to nie ma problemu, ale co jeśli strona będzie bardzo popularna i często odwiedzana? Z tego względu warto oszczędzać zasoby serwera.
Warto też pamiętać, że niektóre serwery mają nałożone ograniczenie na obciążenie procesora. Dużo lepiej jest nawiązać połączenie przy wykonywaniu pierwszego zapytania, które nie jest keszowane.
if(!($this->polaczenie=mysql_connect(HOST, USER, PASS, false, 131072)) || !mysql_select_db(BAZA)) { $this->error='Próba połączenia z bazą danych zwróciła komunikat: '.mysql_error(); return false; }
Musimy też pamiętać, żeby nie nawiązywać połączenia za każdym razem, kiedy wykonujemy jakieś zapytanie. Dlatego przed połączeniem sprawdzamy czy połączenie jest już nawiązane. Czasami zdarza się, że połączenie zerwie się podczas wykonywania skryptu. Aby się przed tym zabezpieczyć, warto używać funkcji mysql_ping, która sprawdza czy połączenie z serwerem działa poprawnie, a jeśli nie, to próbuje je nawiązać ponownie.
if($this->polaczenie && mysql_ping($this->polaczenie)) return true;
Kolejna sprawa to pobieranie i buforowanie wyników zapytań. Bardzo często ludzie stosują funkcję mysql_query. Dużo lepszym rozwiązaniem jest używanie funkcji mysql_unbuffered_query. Nie pobiera ona i nie buforuje wyników zapytania, co daje dodatkowe oszczędności zasobów serwera (głównie pamięci). Po wykonaniu funkcji, warto zapisać wygenerowane dane (np wykorzystując funkcję mysql_fetch_assoc) do danej właściwości klasy, a następnie zwolnić zasoby bazy.
if($wynikZapytania===true) { strtolower(substr($this->zapytanie,0,6))=='insert' && ($this->idRekordu=mysql_insert_id()); $this->liczbaRekordow=mysql_affected_rows(); }else{ while($rekord=mysql_fetch_assoc($wynikZapytania)) { $this->wyniki[]=$rekord; } unset($rekord); is_bool($wynikZapytania) || mysql_free_result($wynikZapytania); $this->liczbaRekordow=count($rekord); }
Kolejną ważną sprawą jest obsługa transakcji. Każdy kto miał do czynienia z transakcjami wie, że są bardzo ważne, ale nie zawsze potrzebne. Czasami stosowanie ich, to jak używanie armaty do zabicia muchy. Nieraz wykonujemy tylko jedno zapytanie ingerujące w bazę danych. W takim wypadku lepiej odpuścić sobie transakcje. Z tego właśnie powodu nie powinno być mechanizmu automatycznego włączania transakcji. Jednak każdy może zmodyfikować ten skrypt i taki mechanizm włączyć.
Dobrze by było, gdyby klasa przy błędzie podczas wykonywania zapytania, sprawdzała czy transakcja była rozpoczęta. Jeśli tak, to klasa powinna ją automatycznie anulować.
if(mysql_unbuffered_query($zapytanie)) return true; $this->error='Błąd w '.$zapytanie.':<br />'.mysql_error(); $this->anulujTransakcje(); return false;
Musimy też pamiętać o sprawdzaniu stanu transakcji przy usuwaniu klasy. Jeśli klasa jest usuwana, a transakcja nadal jest aktywna, to po co ma być włączona, skoro nic już nie będzie w niej zrobione? Dlatego warto wywołać zapytanie ROLLBACK w destruktorze klasy.
Niektórzy zwrócą uwagę na to, że stworzyłem metodę wykonajZapytanie2, która jest odpowiedzialna tylko za wykonywanie zapytania i obsługę błędu. Stworzyłem ją po to, żeby nie pisać kilka razy tego samego kodu. Z kolei wykonywanie metody wykonajZapytanie przy obsłudze transakcji mija się z celem, bo wykonuje się przy tym masa zbędnego kodu.
A oto cała moja klasa do obsługi bazy danych MySql:
class baza { private $polaczenie=0; private $statusTransakcji=0; public $zapytanie=''; public $wyniki=Array(); public $error=''; public $liczbaRekordow=0; public $idRekordu=0; public function __construct() { require_once('inc/ustawieniaBazy.inc.php'); } public function __destrukt() { $this->statusTransakcji) && $this->anulujTransakcje(); $this->polaczenie) && $this->dis connect(); } private function connect() { if($this->polaczenie && mysql_ping($this->polaczenie)) return true; if(!($this->polaczenie=mysql_connect(HOST, USER, PASS, false, 131072)) || !mysql_select_db(BAZA)) { $this->error='Próba połączenia z bazą zwróciła komunikat:'.mysql_error(); return false; } $resources=mysql_unbuffered_query('SET NAMES utf8 COLLATE utf8_general_ci'); is_bool($resources) || @mysql_free_result($resources); return true; } public function disconnect() { if($this->statusTransakcji) $this->rollback(); if($this->polaczenie) { @mysql_close($this->polaczenie); $this->polaczenie=0; } } public function wykonajZapytanie2($zapytanie) { if(mysql_unbuffered_query($zapytanie)) return true; $this->error='Błąd w '.$zapytanie.':<br />'.mysql_error(); $this->anulujTransakcje(); return false; } public function wykonajZapytanie($zapytanie, $keszujZapytanie=false) { $this->zapytanie=trim($zapytanie); $this->numRows=$this->affectedRows=0; $this->dane=Array(); $this->error=''; $this->idRekordu=0; if($keszujZapytanie) { $keszujZapytanie=md5($this->zapytanie); if(file_exists(KESZ.$keszujZapytanie)) { $this->wyniki=unserialize(file_get_contents(KESZ.$keszujZapytanie)); $this->liczbaRekordow=count($this->wyniki); return true; } } $this->connect(); $wynikZapytania=$this->wykonajZapytanie2($this->zapytanie); if($wynikZapytania===true) { strtolower(substr($this->zapytanie,0,6))=='insert' && ($this->idRekordu=mysql_insert_id()); $this->liczbaRekordow=mysql_affected_rows(); }else{ while($rekord=mysql_fetch_assoc($wynikZapytania)) { $this->wyniki[]=$rekord; } unset($rekord); is_bool($wynikZapytania) || mysql_free_result($wynikZapytania); $this->liczbaRekordow=count($rekord); } file_put_contents(KESZ.md5($this->zapytanie), serialize($this->wyniki)); } public function rozpocznijTransakcje() { if(!$this->wykonajZapytanie2('SET autocommit=0') || !$this->wykonajZapytanie2('START TRANSACTION')) return false; $this->statusTransakcji=1; return true; } public function anulujTransakcje() { if(!$this->statusTransakcji) return true; if(!$this->wykonajZapytanie2('ROLLBACK')) return false; $this->statusTransakcji=0; return true; } public function zatwierdzTransakcje() { if(!$this->statusTransakcji) return true; if(!$this->wykonajZapytanie2('COMMIT')) return false; $this->statusTransakcji=0; return true; } }
Na koniec dodam, że taka klasa do obsługi baz danych może być przystosowana do obsługi wielu baz. Wystarczy dodać właściwość oznaczającą typ bazy, a później sprawdzać jaka baza ma być wykorzystana i wykonać odpowiednie instrukcje dla danej bazy danych.