Aktywne rekordy to obiekty, które za pomocą odpowiednich metod możemy wstawiać, wyszukiwać oraz usuwać z bazy danych. Stosując je otrzymamy znacznie bardziej zwięzły i przejrzysty kod.
Organizacja dostępu do bazy danych
Aktywne rekordy są jedną z metod organizacji kodu w bazodanowych aplikacjach pisanych obiektowo. W metodzie tej obiekty hermetyzują dostęp do bazy danych. Klasę zaimplementowaną jako aktywny rekord wzbogacamy o zestaw metod synchronizujących obiekt z bazą danych, dzięki czemu transakcje bazodanowe odbywają się podczas tworzenia i operowania obiektem. Rozwiązanie takie ułatwia nie tylko pisanie kodu samej aplikacji, ale także wprowadzanie ewentualnych zmian.
Idea aktywnych rekordów
Przyjrzyjmy się prostemu przykładowi ilustrującemu aktywne rekordy. Klasa Pracownik:
class Pracownik {
private $FImie;
private $FNazwisko;
}
po przekształceniu w aktywny rekord otrzyma pole {stala}$FId{/stala} oraz metody:
class Pracownik {
private $FId;
private $FImie;
private $FNazwisko;
public function function _construct();
public function set($AImie, $ANazwisko);
public function find($AImie, $ANazwisko);
public function findId($AId);
public function insert();
public function delete();
}
Pole {stala}$FId {/stala}służy do przechowywania numeru identyfikacyjnego rekord, zaś podane metody do synchronizacji dostępu do bazy danych.
Jeśli zechcemy pobrać z bazy danych informacje dotyczące pracownika o numerze identyfikacyjnym 76, to zadanie takie wykonamy następująco:
$prac = new Pracownik();
$prac->findId(76);
Metoda {stala}findId(){/stala} odpowiada za kontakt z bazą danych i za ustalenie pól obiektu (tj. imienia i nazwiska) na podstawie zawartości bazy danych i podanego numeru identyfikacyjnego.
Jeśli natomiast zechcemy wstawić do bazy danych rekord \’Jan Kowalski\’, wówczas należy wywołać metodę {stala}insert(){/stala}:
$prac = new Pracownik();
$prac->set(\'Jan\', \'Kowalski\');
$prac->insert();
Metoda {stala}set(){/stala} ustala wartości pól obiektu na podstawie podanych parametrów. Zapytanie INSERT wstawiające rekord do bazy jest wywoływane wewnątrz metody {stala}insert().{/stala}
Wreszcie usunięcie rekordu \’Anna Nowak\’ realizuje kod:
$prac = new Pracownik();
$prac->find(\'Anna\', \'Nowak\');
$prac->delete();
Metoda {stala}find(){/stala} ustala numer identyfikacyjny rekordu \’Anna Nowak\’ w bazie danych, a metoda {stala}delete(){/stala} wykonuje zapytanie, które usunie rekord.
Powyższy przykład, wprawdzie bardzo uproszczony, dobrze ilustruje zalety aktywnego rekordu.
Po pierwsze kod aplikacji ulegnie znacznemu skróceniu i uproszczeniu. Po drugie operowanie klasą Pracownik nie wymaga znajomości struktury bazy danych ani nawet języka SQL. Dzięki temu zmiany w kodzie aplikacji oraz zmiany w strukturze bazy danych mogą przebiegać niezależnie. Wreszcie dane dotyczące pracownika mogą pochodzić z wielu tabel i być ustalane kilkoma zapytaniami SQL.
Zestaw metod
W ogólnym przypadku aktywne rekordy będą zawierały następujące metody:
- find() – metoda wyszukująca rekord w bazie danych na podstawie danych; np. dane są imię i nazwisko, wyszukujemy numer identyfikacyjny rekordu w bazie danych,
- findId() – metoda wyszukująca rekord w bazie danych na podstawie numeru identyfikacyjnego; np. dany jest numer identyfikacyjny książki, ustalamy pozostałe dane: tytuł, isbn, rok wydania,
- insert() – metoda wstawiająca rekord do bazy danych,
- delete() – metoda usuwająca rekord z bazy danych,
- update() – metoda modyfikująca rekord w bazie danych.
Dodatkowo obiekty będą wyposażone w konstruktor oraz metody dostępu do pól {stala}set(){/stala} oraz {stala}get().{/stala}
Przykład praktyczny
Jako przykład praktyczny wykorzystam aplikację prezentującą zestawienie artykułów Magazynu INTERNET. W rozwiązaniu tym zaimplementowałem sześć klas stosujących metodę aktywnych rekordów:
- klasa Artykul – obiekt tej klasy uzyskuje dostęp do danych pojedynczego artykułu (np. Zakładamy sieć osiedlową) zapisanego w bazie danych,
- klasa Numer – obiekt tej klasy uzyskuje dostęp do danych pojedynczego numeru (np. 7/2004) zapisanego w bazie danych,
- klasa Osoba – obiekt tej klasy uzyskuje dostęp do danych jednej osoby (np. Tomasza Gębarowskiego) zapisanych w bazie danych,
- klasa Rocznik – obiekt tej klasy uzyskuje dostęp do danych dotyczących rocznika (np. 2005) zapisanych w bazie danych,
- klasa Rubryka – obiekt tej klasy uzyskuje dostęp do danych rubryki (np. Temat miesiąca) zapisanych w bazie danych,
- klasa Podrubryka – obiekt tej klasy uzyskuje dostęp do danych podrubryki (np. PHP) zapisanych w bazie danych.
Szczegóły implementacji
Wszystkie przygotowane w przykładzie aktywne rekordy zawierają te same metody. Zestaw metod jest ustalony interfejsem {stala}DBObject{/stala} zawartym w pliku {stala}dbobject.inc.php:{/stala}
interface DBObject
{
public function _construct($ADataSource);
public function set($AArray);
public function find();
public function findId($AId);
public function insert();
}
Parametrem konstruktora {stala}_construct(){/stala} jest obiekt klasy DBA. Wszystkie zapytania SQL są wykonywane za pośrednictwem klasy DBA, która z kolei wykorzystuje klasę PEAR::DB (szczegóły implementacji klasy DBA opisałem w artykule pt. \”Bazy danych, szablony i surowe tablice\”, zaś klasę PEAR::DB w artykule pt. \”PEAR::DB – interfejs dostępu do bazy danych\”).
Metoda {stala}set(){/stala} ustala wartości pól obiektu na podstawie tablicy asocjacyjnej.
Metoda {stala}find(){/stala} wyszukuje w bazie danych rekord o podanych wartościach pól. Przed wywołaniem metody {stala}find(){/stala} należy w polach obiektu ustalić odpowiednie dane (np. imię i nazwisko osoby). Do tego celu wykorzystujemy metodę {stala}set().{/stala} Gdy pola obiektu zawierają dane, metoda {stala}find(){/stala} ustali numer identyfikacyjny rekordu w bazie danych.
Metoda {stala}findId(){/stala} służy do ustalenia danych rekordu na podstawie numeru identyfikacyjnego. Jeśli na przykład zadany jest numer identyfikacyjny autora, to na jego podstawie ustalimy imię, nazwisko oraz listę artykułów danej osoby.
Metoda {stala}insert(){/stala} umieści w bazie danych nowy rekord o zadanych wartościach pól. W ten sposób będziemy do bazy danych dodawali nowe artykuły, rubryki, autorów czy roczniki.
Z racji na to, że obiekty będą przekazywane do szablonu, ich pola mają zasięg publiczny (pola o zasięgu prywatnym nie są widoczne w szablonie). W związku z tym metody get pozwalające na dostęp do pól prywatnych są zbędne.
Przykładowe klasy
Klasa Osoba
Klasa Osoba uzyskuje dostęp do danych pojedynczego autora. Jej szkielet jest przedstawiony na listingu 1.
class Osoba implements DBObject
{
private $Fdb;
public $Fid;
public $Fimie;
public $Fnazwisko;
public $Fartykuly;
public $FWskaznik;
public function __construct($ADataSource)
{
...
}
public function set($AArray)
{
...
}
public function find()
{
...
}
public function findId($AId)
{
...
}
public function insert()
{
...
}
}
Konstruktor klasy zajmuje się wyłącznie ustaleniem obiektu służącego do wydawania zapytań SQL oraz inicjalizacją pola {stala}$Fid.{/stala} Implementacja konstruktora we wszystkich klasach Artykul, Osoba, Rocznik, Numer, Rubryka oraz Podrubryka jest identyczna i wygląda następująco:
public function _construct($ADataSource)
{
$this->Fdb = $ADataSource;
$this->Fid = false;
}
Metoda {stala}set(){/stala} ustala wartości pól {stala}$Fimie{/stala} oraz {stala}$Fnazwisko{/stala} na podstawie zawartości tablicy asocjacyjnej {stala}$AArray{/stala} przekazanej jako parametr:
public function set($AArray)
{
$this->Fimie = $AArray[\'imie\'];
$this->Fnazwisko = $AArray[\'nazwisko\'];
}
Kolejna metoda, {stala}find(){/stala}, ustala numer identyfikacyjny rekordu w bazie danych na podstawie pól {stala}$Fimie{/stala} oraz {stala}$Fnazwisko{/stala}. Swoje działanie opiera na metodzie {stala}DBA_podaj_id_osoby(){/stala} klasy DBA. Po jej wywołaniu w polu {stala}$Fid{/stala} znajduje się albo numer identyfikacyjny rekordu w bazie danych, albo wartość false, jeśli takiego rekordu nie ma:
public function find()
{
$this->Fid = $this->Fdb->DBA_podaj_id_osoby(
$this->Fimie,
$this->Fnazwisko
);
return $this->Fid;
}
Metoda {stala}findId(){/stala} wyszukuje rekord w bazie danych na podstawie podanego numeru identyfikacyjnego {stala}$AId.{/stala} Do wykonania odpowiedniego zapytania SQL wykorzystana jest metoda {stala}DBA_podaj_dane_osoby(){/stala} klasy DBA. Jeśli odpowiedni rekord zostanie znaleziony, to dane (czyli imię i nazwisko) zostaną zapisane do pól {stala}$Fimie{/stala} oraz {stala}$Fnazwisko{/stala}. Dodatkowo, do pola {stala}$Fartykuly{/stala} zostanie zapisana tablica zawierająca dane wszystkich artykułów wybranej osoby, zaś do pola {stala}$FWskaznik{/stala} informacje umożliwiające przewijanie rekordów w bazie danych. Artykuły wybranej osoby wyznaczamy wywołując metodę {stala}DBA_podaj_artykuly_osoby(){/stala}, zaś do przewijania rekordów służy osobna klasa {stala}DBOsobaWskaznik{/stala}:
public function findId($AId)
{
$tmp = $this->Fdb->DBA_podaj_dane_osoby($AId);
if ($tmp === false) {
$this->Fid = false;
return false;
} else {
$this->Fid = $tmp[\'tosoba_id\'];
$this->Fimie = $tmp[\'imie\'];
$this->Fnazwisko = $tmp[\'nazwisko\'];
$this->Fartykuly = $this->Fdb->DBA_podaj_artykuly_osoby($AId);
$this->FWskaznik = new DBOsobaWskaznik($this->Fdb);
$this->FWskaznik->setId($AId);
$this->FWskaznik->update();
return true;
}
}
Ostatnią z zaimplementowanych metod jest {stala}insert(){/stala} – metoda odpowiedzialna za wstawianie rekordów do bazy danych. Przed wywołaniem należy w polach {stala}$Fimie{/stala} oraz {stala}$Fnazwisko{/stala} ustalić dane osoby, które mają zostać umieszczone w bazie danych. Możliwe są cztery sytuacje:
- wstawianie przebiegło pomyślnie, rekord został poprawnie dodany (wynik 1),
- podany rekord już istnieje i nie może być wstawiony ponownie (wynik 2),
- błąd zapytania wstawiającego rekord do bazy danych (wynik 3),
- błąd wyszukiwania rekordu po poprawnie wykonanym wstawianiu (wynik 4).
O zaistniałej sytuacji informuje nas wynik zwracany przez metodę {stala}insert(){/stala}.
Sprawdzenie czy podany rekord istnieje w bazie danych wykonujemy wywołując opisaną już metodę {stala}find(){/stala}, zaś dodawanie rekordu do bazy danych realizujemy za pośrednictwem metody {stala}DBA_wstaw_osobe():{/stala}
public function insert()
{
$this->find();
if (!$this->Fid) {
$tmp = $this->Fdb->DBA_wstaw_osobe(
$this->Fimie,
$this->Fnazwisko
);
if (!$tmp) {
return 3;
}
$this->find();
if (!$this->Fid) {
return 4;
}
return 1;
}
return 2;
}
Klasa Artykul
Klasa Artykul jest nieco bardziej rozbudowana od klasy osoba. Zawiera znacznie więcej pól (zestaw metod jest identyczny):
class Artykul implements DBObject
{
private $Fdb;
public $Fid;
public $Ftytul;
public $Flid;
public $Fstart;
public $Fstop;
public $Frocznik;
public $Frocznik_id;
public $Fnumer;
public $Fnumer_id;
public $Frubryka;
public $Frubryka_id;
public $Fpodrubryka;
public $Fpodrubryka_id;
public $Fautorzy;
public $FWskaznik;
...
}
Z tego powodu metoda {stala}set(){/stala} jest nieco dłuższa:
public function set($AArray)
{
$this->Ftytul = $AArray[\'tytul\'];
$this->Flid = $AArray[\'lid\'];
$this->Fstart = $AArray[\'start\'];
$this->Fstop = $AArray[\'stop\'];
$this->Frocznik = $AArray[\'rocznik\'];
$this->Frocznik_id = $AArray[\'rocznik_id\'];
$this->Fnumer = $AArray[\'numer\'];
$this->Fnumer_id = $AArray[\'numer_id\'];
$this->Frubryka = $AArray[\'rubryka\'];
$this->Frubryka_id = $AArray[\'rubryka_id\'];
$this->Fpodrubryka = $AArray[\'podrubryka\'];
$this->Fpodrubryka_id = $AArray[\'podrubryka_id\'];
}
Pozostałe metody są niemal identyczne jak w przypadku klasy Osoba, różniąc się wyłącznie wywoływanymi metodami klasy DBA oraz liczbą ich parametrów:
public function find()
{
$this->Fid = $this->Fdb->DBA_podaj_id_artykulu(
$this->Ftytul,
$this->Fnumer_id
);
return $this->Fid;
}
Wykorzystanie aktywnych rekordów
Wstawianie rekordów do bazy danych
Kod dodający informacje do bazy danych przy użyciu aktywnych rekordów przyjmie następującą postać:
$rubryka = \'Z OKŁADKI\';
$objRubryka = new Rubryka($db);
$atr = array(
\'rubryka\' => $rubryka
);
$objRubryka->set($atr);
switch ($objRubryka->insert()) {
case 1:
echo \'...\';
break;
case 2:
echo \'...\';
break;
case 3:
echo \'...\';
break;
case 4:
echo \'...\';
break;
}
Po ustaleniu w zmiennej {stala}$rubryka{/stala} nazwy rubryki tworzymy nowy obiekt objRubryka, ustalamy jego dane metodą {stala}set(){/stala}, po czym wywołujemy metodę {stala}insert().{/stala}
Aplikacja pt. \”magazyn INTERNET\”
Skrypt główny aplikacji pt. \”magazyn INTERNET\” jest zawarty w pliku {stala}index.php{/stala}. Z racji na wykorzystanie klasy DBA oraz szablonów Smarty na początku skryptu są tworzone dwa obiekty:
$db = new DBA();
$s = new Smarty;
Obiekty te są w dalszej części wykorzystywane do wydawania zapytań SQL (obiekt {stala}$db{/stala}) oraz do wyświetlenia strony (obiekt {stala}$s{/stala}).
Jeśli zechcemy wyświetlić dane osoby o identyfikatorze 37, to wystarczy wykonać następujący kod:
$id = 37;
$osoba = new Osoba($db);
$osoba->findId($id);
$s->assign(\'osoba\', $osoba);
Zwróćmy uwagę, że do szablonu {stala}$s{/stala} jest przekazywany cały obiekt $osoba.
Dane artykułu o identyfikatorze 159 wyświetlimy:
$id = 159;
$artykul = new Artykul($db);
$artykul->findId($id);
$s->assign(\'artykul\', $artykul);
Tutaj również do szablonu {stala}$s{/stala} przekazujemy obiekt {stala}$artykul{/stala}, nie poddając go żadnym modyfikacjom.
Numer identyfikacyjny rekordu jest przekazywany w zapytaniu HTTP. Stosowane są dwie zmienne URL: id mówiąca o wybranej podstronie oraz id2 zawierająca identyfikator rekordu. Na przykład adres:
index.php?id=7&id2=55
odnosi się do danych osoby o identyfikatorze 55 (id=7 to podstrona zawierająca dane osoby). Zatem kod aplikacji faktycznie przyjmuje postać:
...
case 7:
$osoba = new Osoba($db);
$osoba->findId($_GET[\'id2\']);
$s->assign(\'osoba\', $osoba);
break;
...
Zwróćmy uwagę, że obiekt {stala}$osoba{/stala} zawiera wszystkie konieczne informacje. Oprócz imienia i nazwiska autora także listę jego artykułów oraz dane potrzebne do wykonania wskaźnika następny/poprzedni. Podobnie jest w przypadku pozostałych aktywnych rekordów. Całe aktywne rekordy przekazujemy do szablonu. Jeśli pola klasy mają zasięg publiczny, to w szablonie możemy uzyskać do nich dostęp.
Obiekty odbieramy w szablonie stosując zwykłą notację obiektową znaną z PHP. Na przykład w pliku {stala}index.tpl{/stala} wyświetlamy dane osoby odwołując się do obiektu {stala}$osoba{/stala} w następujący sposób:
{$osoba->Fimie} {$osoba->Fnazwisko}
Pułapki
Pomimo że aktywne rekordy są bardzo wygodne, należy uważać, gdyż ich nadużywanie może prowadzić do dużych nieoptymalności. Przykładem dobrze ilustrującym taki problem jest lista wszystkich rekordów zadanej klasy ewentualnie z warunkiem. W omawianej aplikacji są to między innymi:
- lista wszystkich artykułów wybranego autora,
- lista wszystkich artykułów wybranej rubryki,
- lista wszystkich autorów,
- lista wszystkich rubryk.
Ustalenie listy wszystkich artykułów wybranego autora odbywa się w metodzie {stala}findId(){/stala} klasy Osoba. Stosujmy do tego pojedyncze wywołanie metody {stala}DBA_podaj_artykuly_osoby():{/stala}
$this->Fartykuly = $this->Fdb->DBA_podaj_artykuly_osoby($AId);
W ten sposób wydawane jest pojedyncze zapytanie SQL, wyniki zwrócone w postaci tablicy są przypisane do pola {stala}$Fartykuly.{/stala} Oczywiście żadne dodatkowe obiekty nie są tworzone.
Wykonanie tego samego zadania przy użyciu aktywnych rekordów klasy Artykul wymagałoby:
- ustalenia listy identyfikatorów artykułów wybranej osoby,
- utworzenia jednego obiektu klasy Artykul dla każdego artykułu,
- ustalenia danych każdego artykułu, poprzez wywołanie metody {stala}findId().{/stala}
Zatem zamiast jednego zapytania SQL wykonalibyśmy liczbę zapytań równą liczbie artykułów plus jeden, oraz utworzylibyśmy po jednym obiekcie dla każdego artykułu.