Archive

Archive for the ‘ORM’ Category

Zend Framework i keszowanie zapytań do bazy danych

June 23rd, 2007 3 comments
Witam, Od pewnego czasu programiści PHP naśladują programistów Javy, sam zresztą to robię. Efektem tego jest przenoszenie wzorców projektowych z Javy do PHP. Na przykład Zend Framework a w szczególności jego kod odpowiedzialny za bazy danych – Zend_Db (działający prawie jak ORM) przypomina w pewien sposób to, za co świat Javy pokochał Hibernate. Konsekwencją użycia ORM w projekcie jest to, że spada nam złożoność zapytań do bazy danych, natomiast zwiększa się ich ilość. Hibernate potrafi keszować obiekty. Zend Framework niestety nie. Jednak wczoraj tego bardzo potrzebowałem, renderowanie prostej strony WWW wymagało 500 zapytań do bazy, grrr, z czego 450 było powtórzeniem poprzednich zapytań. O ile keszowanie obiektów było by bardzo skomplikowane, to w miarę prosto można było keszować wyniki zapytań SQL w obrębie jednego żądania HTTP. Jak się do tego zabrać? Zauważyłem, że wszystkie zapytania do bazy danych produkowane przez Zend Framework w pewnym momencie trafią do metody query obiektu Zend_Db_Adapter_*, fajnie:). Naszą klasę keszującą trzeba odziedziczyć po tej klasie. Oczywiście można to zrobić na wiele innych sposobów, i bardzo ładnie wpasować w to wzorzec projektowy dekorator, ale ja nie miałem czasu. Modyfikujemy metodę query, tak by zapamiętywała swoje wyniki i trzymała w keszu. U mnie działa coś takiego (wstydził bym się tego kodu, gdyby nie to że robi coś na prawdę przydatnego):
class JakubiakDbAdapterPdoPgsql extends Zend_Db_Adapter_Pdo_Pgsql{
  public function query($sql, $bind = array()){
    $sql = $sql . ""; // a sprobuj no bez tego
    $hash = $this->_calculateHash($sql,$bind);
    // czy uda nam sie pobrac z kesza
    if($this->_isCacheEnabled) {
      if($this->_allowCache($sql,$bind)) {
        if(isset($this->_cachedResults[$hash])) {
          return $this->_cachedResults[$hash];  
        } 
      }
    }
    $res = parent::query($sql, $bind);
    // dopisanie do kesza
    if($this->_isCacheEnabled) {
      $this->_cachedResults[$hash] = $res;
    }
    return $res;
  }
  // pozwalam na keszowanie tylko zapytan rozpoczynajacych sie od select
  private function _allowCache($sql,$bind) {
    return preg_match("/^select/i",$sql));
  }
  private function _calculateHash($sql,$bind) {
    return md5($sql.var_export($bind,true));
  }
  [...]
}
Ten kod omal nie zadziała. Jednak trzeba zrobić jeszcze parę myków. Przeciążyć konstruktor naszego adaptera.
  public function __construct($config) {
    parent::__construct($config);
    $this->getConnection()->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('JakubiakDbStatement', array($this)));
  }
I dopisać taką oto koszmarną klasę:
class JakubiakDbStatement extends PDOStatement {
  $_cache = null;
  public function fetchAll($how, $class_name=null, $ctor_args=null) {
    if(!empty($this->_cache)) {
      return $this->_cache;
    }
    $this->_cache = parent::fetchAll($how);
    return $this->_cache;
  }
}
Ten kod, ma prawo nie działać i zawiera błędy. Jednak u mnie działa. I to dobrze działa. Zamiast 500 zapytań do bazy mam ich 50. Serwer jest mi za to wdzięczny, nie trzeba już oliwić wentylatorka od chłodzenia;). PS. do testów użyłem starej wersji Zend_Framework – 0.9.1.
Categories: ORM, PHP, PostgreSQL, Zend Framework Tags:

Hibernate i logowanie zapytań SQL wraz w wartościami parametrów

June 1st, 2007 No comments
Witam. Hibernate potrafi logować zapytania SQL. Można to skonfigurować na kilka sposobów. Ja wybrałem konfiguracje z log4j:
# logowanie zapytań SQL
log4j.logger.org.hibernate.SQL=DEBUG, R
log4j.additivity.org.hibernate.SQL=false
# logowanie wartości wstawianych do SQL
log4j.logger.org.hibernate.type=DEBUG
Pozdrawiam!
Categories: JPA, ORM Tags:

Zend Framework, zachciało mi się HQL’a

April 22nd, 2007 1 comment
Zend Framework – framework, o którym ostatnio sporo pisałem. HQL język używany w Hibernate, będący połączeniem ciemności świata relacji i światłości obiektów. HQL było inspiracją dla JPQL , dzięki czemu stanie się standardem przemysłowym. HQL jest fajny, bo można pisać na przykład tak:
from Movie m where m.media.user.name='Antek';
zamiast dłubać w SQL:
select m.* from movie v 
join media m on m.mediaid=v.mediaid 
join user u on u.userid=m.userid 
where user.name='Antek';
Niestety, Zend Framework nie posiada czegoś takiego i nie zanosi się na to. Ja, jednak klepnąłem świniaka, dzięki któremu mogę pisać podobnie. Zmodyfikowałem klasę: Zend_Db_Table ^ JakubiakDbTable. Nowy kod to:
protected function _fetch($where = null, $order = null, 
$count = null, $offset = null) {
  $select = $this->_db->select();
  $select->from($this->_name, $this->_cols);
  $where = (array) $where;
  foreach ($where as $key => $val) {
    if (is_int($key)) {
      $select->where($val);
    } else {
      // drobna modyfikacja rodzica
      $key = $this->refJoin($select,$key);
      $select->where($key, $val);
    }
  }
  // dalej tak jak u rodzica[...]
}
public function refJoin($select, $where) {
  foreach($this->_referenceMap as $key => $ref) {
    $regExp = "#^$key\.#";
    if (preg_match($regExp,$where)) {
      $refDao = new $ref['refTableClass'];
      $on = "";
      foreach($ref['columns'] as $i => $colName){
        if($i>0) $on .= ' and ';
        $on .= ' ';
        $on .= $this->getTableName() . '.' . $colName . '=';
        $on .= $refDao->getTableName() . '.' .$ref['refColumns'][$i] . ' ';
      }
      $select->join($refDao->getTableName(), $on,array());
      $where = preg_replace($regExp,"",$where);
      return $refDao->refJoin($select,$where); 
    }
  }
  return $this->getTableName().'.'.$where;
}
public function getTableName() {
  return $this->_name;
}
Kod powyżej działa, ale jest zły. Nie używaj go. Ja mogę go używać tak:
$movieTable = new MovieTable();
$movies = $movieTable->fetchAll(array("media.user.name=?"=>$_REQUEST['username']));
Czyli, prawie, prawie jak HQL.
Categories: ORM, PHP, Zend Framework Tags:

Zend Framework i PostgreSQL aka ORM

April 21st, 2007 No comments
Proponuje kilka moich poprawek do Zend Framework 0.9.2. Dzięki temu praca z tą biblioteką jest jeszcze przyjemniejsza. Wersja 0.9.2 ma parę niedociągnięć, jednak można je poprawić poprzez dziedziczenie. Zacznę od klasy Zend_Db_Adapter_Pdo_Pgsql PostgreSQL. Pisałem już o niej. Proponuje następujące ulepszenia:
class JakubiakDbAdapterPdoPgsql extends Zend_Db_Adapter_Pdo_Pgsql{
  public function query($sql, $bind = array()){
    // poprawienie błędu dla typu Boolean
    if(is_array($bind)){
      foreach($bind as $k => $v) {
        if(is_bool($v)) {
          $bind[$k] = $v ? 't' : 'f';
        }
      }
    }
    return parent::query($sql, $bind);
  }
  public function lastInsertId($tableName = null) {
    // Jeżeli sekwencja nazywasz inaczej niż domyślnie, to prawdopodobnie trzeba
    // będzie nadpisać też tą funkcję, u mnie wygląda ona tak 
    // sekwencje dla kluczy głównych nazywam {nazwa_tabelki}_seq
    if (!$tableName) {
      throw new Zend_Db_Adapter_Exception("Sequence name must be specified");
    }
    $this->_connect();
    $sequenceName = "{$tableName}_seq";
    $sequenceName = preg_replace('/_+/','_',$sequenceName);
    return $this->_connection->lastInsertId($sequenceName);
  }
Teraz zabawię się z klasą która reprezentuje tabelę Zend_Db_Table. Jest ona podwaliną ORM, dlatego mi się podoba:
class JakubiakDbTable extends Zend_Db_Table {
  // chcę, aby wszystkie wiersze w tabelkach były reprezentowane przez
  // obiekty tej klasy
  protected $_rowClass = 'JakubiakDbTableRow';
  // następnie poprawiam błąd - lastInsertedId
  public function insert(array $data)  {
    $this->_db->insert($this->_name, $data);
    return $this->_db->lastInsertId(empty($this->_seqName)
      ? $this->_name : $this->_seqName);
  }
  // przydatna funkcja do zliczania wierszy w tabeli
  public function count(array $where = array()) {
    $select = $this->_db->select();
    $select->from($this->_name, array('count'=>'count(*)'));
    $where = (array) $where;
    foreach ($where as $key => $val) {
      if (is_int($key)) {
        $select->where($val);
      } else {
        $select->where($key, $val);
      }
    }
    $select->limit(1, 0);
    $stmt = $this->_db->query($select);
    $data = $stmt->fetch();
    return empty($data['count']) ? 0 : intval($data['count']);
  }
  // pobieranie nowej encji
  public function fetchNew() {
    $newRow = parent::fetchNew();
    $newRow->loadDefaults();
    return $newRow;
  }
  // to się jeszcze przyda, aczkolwiek wydaje mi się, że ta funkcja trafi prędzej czy 
  // później do core
  public function getReferenceMap($key){
    if(empty($this->_referenceMap[$key])){
      return null;
    }
    return $this->_referenceMap[$key];     
  }
}
I jeszcze klasa która reprezentuje encje Zend_Db_Table_Row – lub jak kto woli – wiersz w tabeli:
class JakubiakDbTableRow extends Zend_Db_Table_Row {
  // czy jesteśmy nową encją?
  private $_isNew = false;
  // wczytanie domyślnych wartości, na podstawie metadanych zapisanych w tabeli
  public function loadDefaults() {
    $db = $this->_getTable()->getAdapter();    
    $info = $this->_getTable()->info();
    foreach($info['metadata'] as $col => $meta){
      if(empty($meta['DEFAULT'])) continue;
      $default = $meta['DEFAULT'];
      $one = $db->query("select $default as def")->fetch();
      $this->_data[$col] = $one['def'];
    }
    $this->_isNew = true;
  }
  // zapisywanie rekordu do bazy, kod prawie taki sam jak w klasie nadrzędnej,
  // ale wykorzystuje prywatną zmienną isNew
  public function save() {
    $keys = $this->_getPrimaryKey();
    $values = array_filter($keys);
    if ($this->_isNew) {
      $this->_insert();
      $result = $this->_getTable()->insert($this->_data);
      if (is_numeric($result)) {
        $this->_data[key($keys)] = $result;
        $this->_refresh();
      }
    } else {
      $where = $this->_getWhereQuery(false);
      $this->_update();
      $depTables = $this->_getTable()->getDependentTables();
      if (!empty($depTables)) {
        $db = $this->_getTable()->getAdapter();
        $pkNew = $this->_getPrimaryKey(true);
        $pkOld = $this->_getPrimaryKey(false);
        $thisClass = get_class($this);
        foreach ($depTables as $tableClass) {
          Zend_Loader::loadClass($tableClass);
          $t = new $tableClass(array('db' => $db));
          $t->_cascadeUpdate($this->getTableClass(), $pkOld, $pkNew);
        }
      }
      $result = $this->_getTable()->update($this->_data, $where);
      if (is_int($result)) {
        // update worked, refresh with data from the table
        $this->_refresh();
      }
    }
    return $result;
  }
  // pobieranie many to one w bardziej intuicyjny sposób
  public function __get($key) {
    $r = $this->_getTable()->getReferenceMap($key);
    if (!empty($r)) {
      // tu można dopisać loader dla klasy 
      return $this->findParentRow($r['refTableClass'],$key);
    }
    return parent::__get($key);
  }
  // domyślnie setter zabrania zmieniać wartości klucza głównego, 
  // ale czasami chcę to robić, kod prawie taki sam jak rodzica
  public function __set($key, $value) {
    if (!$this->_isNew && in_array($key, $this->_primary)) {
      require_once 'Zend/Db/Table/Row/Exception.php';
      throw new Zend_Db_Table_Row_Exception("Changing the primary key value(s) is not allowed");
    }
    if (!array_key_exists($key, $this->_data)) {
      require_once 'Zend/Db/Table/Row/Exception.php';
      throw new Zend_Db_Table_Row_Exception("Specified column \"$key\" is not in the row");
    }
    $this->_data[$key] = $value;
  }
Po tej zabawie, mogę używać bibliotek na przykład tak:
$fileTable = new FileTable();
$fileRow = $fileTable->fetchNew($_FILES[$filename]);
$fileRow->save();
$mediaTable = new MediaTable();
$mediaRow = $mediaTable->fetchNew();
$mediaRow->mediatitle = $request->getParam('mediatitle');
$mediaRow->mediadescription = $request->getParam('mediadescription');
$mediaRow->fileid = $fileRow->fileid;
$mediaRow->userid = UserTable::getFromAuth()->userid;
$mediaRow->save();
$photoTable = new PhotoTable();
$photoRow = $photoTable->fetchNew();    
$photoRow->mediaid = $mediaRow->mediaid;
$photoRow->photowidth = $width;
$photoRow->photoheight = $height;
$photoRow->save();
echo $photoRow->media->file->fileid;
Miodzio? Nie chcę wracać do czasów, gdy nie znałem ORM. Czuję się prawie jak w JPA. Acha, nie zapomnij o przeczytaniu dokumentacji Zenda bo jest bardzo dobra.
Categories: ORM, PHP, PostgreSQL, Zend Framework Tags:

Zend Framework – Zend_Db_Table

April 9th, 2007 1 comment
Krytykowałem już Zend Framework za zmienność. Dziś go pochwalę. Bardzo podoba mi się Zend_Db_Table. Jest to proste, ale prawdziwe OR/M. W dodatku, żeby go używać nie trzeba się sporo napisać i nie trzeba się wiele uczyć. Aktualizacja: 2007-04-10. Cholera! Jest sobie funkcja Zend_Db_Table_Abstract::insert() – ale nie działą dla PDO_PGSQL. Kod źródłowy, sprawia wrażenie nigdy nie testowanego. W dodatku jest nie poprawny merytorycznie. Aż zacytuje:
    public function insert(array $data)
    {
        $this->_db->insert($this->_name, $data);
        return $this->_db->lastInsertId();
    }
    public function lastInsertId($tableName = null, $primaryKey = 'id')
    {
        if (!$tableName) {
            throw new Zend_Db_Adapter_Exception("Sequence name must be specified");
        }
Zend Framework jest fajny bo nowatorski. Niestety, jest jeszcze w stadium mocno rozwojowym. Ten błąd na szczęście mogę poprawić w swojej aplikacji bez modyfikacji kodu źródłowego bibliotek Zend. Ale ile takich błędów jeszcze znajdę?
Categories: ORM, PHP, Zend Framework Tags:

Pierwsza noc z Hibernate

June 2nd, 2006 9 comments

Hibernate zafascynował mnie od chwili, gdy pierwszy raz ujrzałem dokumentacje. Zamieszczone przykłady obiecywały mi rozwiązanie najgorszych problemów programowania w PHP. Wczoraj spędziłem noc z Hibernate i muszę przyznać, że jestem zachwycony. Hibernate jest lepszy, niż to sobie wyobrażałem. Dokonałem cudu. Udało mi się osiągnąć to, co chciałem.

Hibernate to biblioteka programistyczna umożliwiająca mapowania obiektowo relacyjne.

Każdy programista PHP zdaje sobie sprawę z konieczności pisania SQL, które będą pobierać dane z bazy, modyfikować rekordy lub robić inne dziwne operacje na relacjach. W PHP zwykle osadza się kod SQL gdzieś tam w skrypcie. Niekiedy, piszę się procedury, które budują bardziej skomplikowany kod SQL, po to tylko, żeby za moment wykonać go w bazie danych. Ten rodzaj pracy przyczynia się do powstawania błędów. Rozwiązaniem problemu, jest przeniesienie skryptów SQL do osobnego miejsca – wyodrębnie SQL od kodu PHP.

Hibernate rozwiązuje ten problem jeszcze lepiej. Nie trzeba pisać zapytań SQL. Wystarczy stworzyć obiekty, klasy Java, a następnie opisać w jakis sposób te obiekty mają być mapowane na pola w relacyjnej bazie danych. To hibernate dba o to, by w bazie danych znalazły się aktualne rekordy. Hibernate aktualizuje także strukturę bazy danych. Ze smutkiem przyznaje, że gigantyczna porcja mojej wiedzy dotyczącej programowania bazy PostgreSQL w obliczy technologi ORM jest mi właściwie zbędna. Ale jako początkujący programista ORM jestem w stanie napisać lepszy program, niż jako zaawansowany programista PostgreSQL.

Categories: JPA, ORM Tags: