czwartek, 12 kwietnia 2007

Zend Framework - obsługa baz danych


Przyznam szczerze, że na Zend Framework trafiłem dzięki praktykom w IBM, gdzie przyszło mi testować hybrydowy silnik baz danych (pureXML w DB2 9). Postanowiłem pobawić się bowiem serwerem Zend Core for IBM (dość szybko przerzuciłem się na Zend Core 2) i przy jego instalacji zauważyłem ZF.
W dobie programowania obiektowego mamy szeroki wybór API, które zdeserializują nam rekord tablicy na obiekt klasy, pozwolą go zmodyfikować w łatwy sposób i dokonać ponownej serializacji. Sztandarowym przykładem jest na chwilę obecną zapewne Java Persistence API opisywana szczegółowo przez Jacka Laskowskiego, ale oczywiście takiej możliwości w Zend Framework zabraknąć nie mogło.
Postanowiłem zatem omówić ten mechanizm i zwrócić uwagę na pewną ważną moim zdaniem kwestię - unikanie deserializacji do obiektów i wykorzystanie tablic bądź tablic asocjacyjnych tam (w Javie byłoby to używanie kolekcji lub tablic typów wbudowanych), gdzie istotne jest wydajne przetwarzanie informacji.


W moim przykładzie posłużę się tabelą opisującą pęk promieni kosmicznych, podobną do tej z którą pracuję na co dzień:

CREATE TABLE cosmic_rays(
id serial NOT NULL, -- Automatycznie generowany identyfikator
nix_date float8, -- Unix'owy stempel czasu
peak_particles float 8, -- Liczba zarejestrowanych cząstek
az float4, -- Położenie źródła we współrzędnych horyzontalnych - azymut
ze float4, -- Położenie źródła we współrzędnych horyzontalnych - odległość od zenitu
ra float4, -- Położenie źródła we współrzędnych niebieskich - rekt ascencja
dec float4, -- Położenie źródła we współrzędnych niebieskich - deklinacja
l float4, -- Położenie źródła we współrzędnych galaktycznych - szerokość gal
b float4, -- Położenie źródła we współrzędnych galaktycznych - długość gal
CONSTRAINT id PRIMARY KEY (id)
) WITHOUT OIDS;

Jak łatwo się domyślić tabela jest przygotowana dla bazy opartej o PostgreSQL, ale nie trzeba wielu zabiegów, aby przystosować ją do innego systemu baz danych.


Struktura katalogów w projekcie


Podobnie jak w poprzednim przykładzie opartym o Zend Framework struktura będzie wyglądać w następujący sposób:


  • application

    • controllers

      • IndexController.php


    • models

      • CosmicRays.php


    • views


  • configuration

    • config.xml


  • library

    • Zend

    • Zend.php


  • public

  • .htaccess

  • index.php



Plik .htaccess zawiera również jedynie informacje sterujące dla modułu rewrite:

RewriteEngine on
RewriteRule .* index.php



Inicjalizacja połączenia z bazą danych


Plik config.xml będzie zawierać niezbędne do zestawienia połączenia z bazą danych informacje:

<?xml version="1.0" encoding="utf-8">
<configData>
<db>
<adapter>PDO_Pgsql</adapter>
<configuration>
<host>localhost</host>
<port>5432</port>
<dbname>baza</dbname>
<username>uzytkownik</username>
<password>*****</password>
</configuration>
</db>
</configData>

Możemy zatem przystąpić do konfiguracji bootstrapa w pliku index.php:

//Ustawiamy ścieżki dostępu
set_include_path('.' . PATH_SEPARATOR
. './library' . PATH_SEPARATOR
. './application/models/' . PATH_SEPARATOR
. get_include_path() );

//Ładujemy klasę Zend_Loader
require_once 'Zend/Loader.php';

try{

//Ładujemy niezbędne klasy
Zend_Loader::loadClass('Zend_Config_Xml');
Zend_Loader::loadClass('Zend_Db');
Zend_Loader::loadClass('Zend_Db_Table');

Zend_Loader::loadClass('Zend_Registry');
Zend_Loader::loadClass('Zend_View');
Zend_Loader::loadClass('Zend_Controller_Front');

//Ustalamy adres bazowy
$baseUrl = substr($_SERVER['PHP_SELF'], 0,
strpos($_SERVER['PHP_SELF'], '/index.php'));

//Ładujemy konfigurację bazy danych z pliku XML (gałąź db)
$config = new Zend_Config_Xml('./configuration/config.xml','db');

//Tworzymy połączenie dla bazy danych i czynimy je domyślnym dla modeli tabel
$db = Zend_Db::factory($config->adapter,
$config->configuration->asArray());
Zend_Db_Table::setDefaultAdapter($db);


//Konfigurujemy klasę widoku
$view = new Zend_View();
$view->baseUrl = $baseUrl;
$view->setScriptPath('./application/views');
//Umieszczamy ją w rejestrze
Zend_Registry::set('view',$view);

//Inicjalizacja głównego kontrolera
$frontController = Zend_Controller_Front::getInstance();
$frontController->setRouter($router);
$frontController->setBaseUrl($baseUrl);
$frontController->setControllerDirectory(
'./application/controllers');
$frontController->throwExceptions(true);
$frontController->returnResponse(true);

//Inicjalizacja obiektu odpowiedzi
$response = $frontController->dispatch();
$response->renderExceptions(true);
$response->setHeader('Pragma', 'No-cache');
$response->setHeader('Cache-Control', 'no-cache');

//Wyświetlenie strony
echo $response;

} catch (Exception $e){
//Obsługa wyjątków
die($e->getMessage());
}

Jak widać dodanie obsługi naszej bazy danych wymagało od nas 6 linijek kodu (wraz z importem plików odpowiednich bibliotek).
Obiekt Zend_Config_Xml (dziedziczący po Zend_Config) jest obiektem pozwalającym na łatwe ładowanie plików konfiguracyjnych w postaci dokumentów XML. Jego użycie jest bardzo podobne do obsługi plików XML przy pomocy SimpleXML (patrz wcześniejszy post).
Inicjalizacji połączenia dokonujemy przez statyczną metodę factory klasy Zend_Db, której podajemy typ adaptera (dla większości baz są to adaptery PDO, własne adaptery posiadają jedynie DB2, Oracle i Mysqli) oraz parametry konfiguracyjne.
Z kolei statyczną metodą setDefaultAdapter klasy Zend_Db_Table ustawiamy utworzone połączenie dla wszystkich obiektów związanych z tabelami.


Nasz pierwszy model


W pliku CosmicRays.php umieścimy nasz model dla tabeli cosmic_rays:

Zend_Loader::loadClass('Zend_Db_Table');

class CosmicRays extends Zend_Db_Table{
protected function _setup(){
//Standardowo ZF ustala nazwę tabeli jako łańcuch znaków występujący
//po ostatnim znaku _ w nazwie klasy modelu, więc musimy ją zmienić
$this->_name = 'cosmic_rays';
//Możemy również ustalić nazwę klucza głównego (jeśli inna niż id)
//oraz schemat tabeli (szczególnie ważne dla baz takich jak DB2 czy Oracle)

//Na koniec ładujemy ustawienia klasy bazowej
parent::_setup();
}
}

I to by było na tyle. Musimy tylko stworzyć obiekt klasy CosmicRays i już możemy modyfikować jej obiekty. Do dyspozycji mamy metody (wymieniłem najważniejsze):

  • fetchNew() - tworzy nowy pusty "rekord"

  • fetchRow($where, $order) - zwraca pierwszy "rekord" z wyniku spełniającego warunek $where z uporządkowaniem $order

  • fetchAll($where, $order) - zwraca wszystkie "rekordy"

  • insert($data) - wstawia "rekord" na podstawie tablicy asocjacyjnej

  • update($data, $where) - aktualizuje dane na podstawie tablicy asocjacyjnej


Warto zwrócić uwagę, iż metoda fetchAll zwraca nam obiekt typu Zend_Db_Table_Rowset z iteratorem dla obiektów Zend_Db_Table_Rows (dzięki temu możliwe jest przeglądanie kolekcji za pomocą pętli foreach). Metody fetchNew i fetchRow zwracają nam obiekt typu Zend_Db_Table_Row.
Każdy z obiektów Zend_Db_Table_Row udostępnia nam dane w sposób podobny do obiektów stClass. Możemy zatem modyfikować je w łatwy sposób (niedopuszczalna jest jedynie modyfikacja klucza głównego), np.

$cr = new CosmicRays();
$row = $cr->fetchRow("id = 10");
$row->ra = 0;
$row->update();

Przytoczona powyżej metoda update() daje nam możliwość zaktualizowania obiektu. Istnieje również metoda save() pozwalająca na zapis nowego obiektu utworzonego za pomocą metody fetchNew().


Problem wydajności


Problemem na jaki natrafiłem było pobranie jedynie dwóch kolumn z całej tabeli. Oczywiście możemy stworzyć obiekt select. Jednak wymaga to umieszczenia obiektu $db w rejestrze, bądź wyłuskania go z obiektu modelu.
Oczywiście dla zapytań łączących dane z wielu tabeli jest to praktycznie jedyna metoda. Co jeśli jednak chcemy dane wyłuskać TYLKO z jednej tabeli?

Stwórzmy nową metodę w klasie modelu:

public function fetchAllColumns($columns = null, $where = null, $order = null,
$count = null, $offset = null){

// selection tool
$select = $this->_db->select();

//Set columns
$columns = (is_null($columns))?$this->_cols:$columns;


// the FROM clause
$select->from($this->_name, $columns);

// the WHERE clause
$where = (array) $where;
foreach ($where as $key => $val) {
// is $key an int?
if (is_int($key)) {
// $val is the full condition
$select->where($val);
} else {
// $key is the condition with placeholder,
// and $val is quoted into the condition
$select->where($key, $val);
}
}

// the ORDER clause
if (!is_array($order)) {
$order = array($order);
}
foreach ($order as $val) {
$select->order($val);
}

// the LIMIT clause
$select->limit($count, $offset);

// return the results
$stmt = $this->_db->query($select);
$data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);

return $data;
}

Osoby, które przyglądały się klasie Zend_Db_Table_Abstract zauważą szybko, że moja metoda nie jest niczym innym jak lekko zmodyfikowaną metodą _fetch(...).
Warto jednak spojrzeć na implementacje metody fetchAll(...) tej klasy. Otóż tablicę asocjacyjną opakowuje ona w obiekt Zend_Db_Table_Rowset. W sytuacjach kiedy zależy nam na wydajności oraz niskim zapotrzebowaniu na pamięć warto zrezygnować z ułatwienia jakie niesie za sobą klasa Zend_Db_Table_Rowset.

Na koniec jeszcze jedna rada. Warto korzystać z instrukcji unset($data), szczególnie jeśli obiekt $data jest pokaźnych rozmiarów tablicą, której dalej nie zamierzamy używać. Tymbardziej, że często administratorzy serwerów nie dają dla skryptów php więcej niż 16MB pamięci.

Brak komentarzy: