Db_Table extends Zend_Db_Table_Abstract czyli..
Postanowiłem jeszcze bardziej zautomatyzować proces przepływu danych dla klas wygenerowanych za pomocą Propela, dziedziczących funkcjonalność po Zend_Db_Table_Abstract.
W ten sposób powstała klasa Db_Table (tak po prostu), która oferuje:
- rekurencyjne dodawanie danych do tabeli bazowej i związanych z nią kluczami obcymi tabel
- rekurencyjne update’owanie tabeli bazowej i związanych z nią kluczami obcymi tabel
- metody zwracające wszystkie dane (wiersz) z tabel powiązanych kluczem obcym z tabelą bazową (automatyczne LEFT JOINY).
Z pewnością zawiera jeszcze pewne niedoskonałości ale zostaną one sukcesywnie poprawiane a features’y rozszerzane.
Oto ona:
<?php
/**
* @author Zenedith
*
*/
class Db_Table extends Zend_Db_Table_Abstract
{
const ERR_UPDATE = 'err_update';
const ERR_DUPLICATE = 'err_duplicate';
const NO_DUPLICATE = 'no_duplicate';
const CHECK_DUPLICATE_BASE_TABLE = 1;
const CHECK_DUPLICATE_BASE_AND_FOREIGN_TABLE = 2;
const CHECK_DUPLICATE_FOREIGN_TABLE = 3;
const UPDATE_BASE_TABLE = 11;
const UPDATE_BASE_TABLE_AND_FOREIGN_TABLE = 12;
const UPDATE_FOREIGN_TABLE = 13;
/**
* Update'uje dane dla tabel powiaznaych z kluczami obcymi, jesli sa odpwiednie pola w tablicy
* @param array $UPDATE - tablica danych dla tabel obcych
*/
protected function updateForeignTable(array $UPDATE)
{
if (!empty($this->_referenceMap) && !empty($UPDATE)) {
foreach ($this->_referenceMap as $ref => $REF) {
//jesli nie ustawiono id dla update, nie updateuj
if (!isset($UPDATE[$REF['columns'][0]]) || !$UPDATE[$REF['columns'][0]]) {
return false;
}
$table = new $REF['refClass']();
$table->update($UPDATE, $table->_primary.' = '.$UPDATE[$table->_primary]);
}
}
return true;
}
/**
* Zapisuje dane dla tabel powiaznaych z kluczami obcymi.Jesli tworzy wpis dla tablicy
* zwiazanej z kluczem, to go zapisuje w danych, ktore trafia do zapisu tablicy wywolujacej
* @param array $INSERT - referncja - tablica danych tabeli wywolujacej, jest uzupelniania o
* informache o kluczu z insertow z tabel
* @param array $REST - tablica wartosci niepasujacych do tabeli wywolujacej, propagowane
* dalej w celu zapisu do tabel powiazanych kluczami obcymi
*/
protected function insertForeignTable(array &$INSERT, array $REST)
{
if (!empty($this->_referenceMap) && !empty($REST)) {
foreach ($this->_referenceMap as $ref => $REF) {
//nie nadpisuj wartosci klucza obcego jesli jest ustawiona
if (isset($INSERT[$REF['columns'][0]]) && $INSERT[$REF['columns'][0]]) {
continue;
}
$table = new $REF['refClass']();
$inserted_id = $table->insert($REST);
if ($inserted_id) {
$INSERT[$REF['columns'][0]] = $inserted_id;
}
}
}
}
/**
* Sprawdza duplikacje danych dla tabel z kluczy obcych
* @param array $REST
* @return bool
*/
protected function checkDuplcatesForeignTable(array $REST)
{
if (!empty($this->_referenceMap) && !empty($REST)) {
foreach ($this->_referenceMap as $ref => $REF) {
$table = new $REF['refClass']();
list($REST_INSERT, $REST_FOREIGN) = $table->splitDataForTables($REST);
$result = $table->checkDuplicate($REST_INSERT);
if ($result) {
return true;
}
}
}
return false;
}
/**
* Dzieli przekazana tablica danych na tablice danych pasujacych do danej tabeli oraz reszte
* @param array $DATA - tablica danych
* @return array($OWN, $FOREIGN)
*/
protected function splitDataForTables(array $DATA)
{
$OWN = array();
$FOREIGN = array();
if (!empty($DATA)) {
foreach ($DATA as $col_name => $col_value) {
if (in_array($col_name, $this->_cols)) {
$OWN[$col_name] = $col_value;
}
else {
$FOREIGN[$col_name] = $col_value;
}
}
}
return array($OWN, $FOREIGN);
}
/**
* Update'uje dane dla tabeli wywolujacej oraz dla tabel powiazanych kluczem obcym
* @param array $data - tablica z danymi do zapisu
* @param $where - opcjonalnie - warunek where dla update'u (jesli nie bedzie podany
* zostanie podjeta proba jego utworzenia na podtawie informacji o id z przekazanej tablicy $data)
* @param $update_flag - opcjonalnie - flaga dla ustawien update'u tabel
*/
public function update(array $data, $where = false, $update_flag = self::UPDATE_BASE_TABLE)
{
$UPDATE = array();
$UPDATE_FOREIGN = array();
$ok = true;
$this->_db->beginTransaction();
try {
list($UPDATE, $UPDATE_FOREIGN) = $this->splitDataForTables($data);
//jesli where nie zostal podany to sprawdz czy z tablicy tego nie wyciagniesz
if (!$where && isset($UPDATE[$this->_primary]) && $UPDATE[$this->_primary]) {
$where = $this->_primary.' = '.$UPDATE[$this->_primary];
}
if ($update_flag == self::UPDATE_FOREIGN_TABLE || $update_flag == self::UPDATE_BASE_TABLE_AND_FOREIGN_TABLE) {
$ok = $this->updateForeignTable($UPDATE_FOREIGN);
}
if ($ok && !empty($UPDATE) && $where) {
if ($update_flag == self::UPDATE_BASE_TABLE || $update_flag == self::UPDATE_BASE_TABLE_AND_FOREIGN_TABLE) {
parent::update($UPDATE, $where);
}
$ok = true;
}
else {
$ok = self::ERR_UPDATE;
}
// przenosimy na koniec zeby zawsze sie wykonal
// alternatywnie mozna zmodyfikowac funkcje rollback
// $this->_db->commit();
}
catch (PDOException $e) {
$this->_db->rollBack();
$ok = self::ERR_UPDATE;
}
catch (Exception $e) {
$this->_db->rollBack();
$ok = self::ERR_UPDATE;
}
$this->_db->commit();
return $ok;
}
/**
* Zapisuje dane dla tabeli wywolujacej oraz dla tabel powiazanych kluczem obcym
* @param array $data - tablica z danymi do zapisu
* @param array $duplicate_check_flag - opcjonalnie - flaga dla sprawdzania duplikacji
*/
public function insert(array $data, $duplicate_check_flag = false)
{
$last_inserted_id = 0;
$INSERT = array();
$FOREIGN = array();
$result = false;
try {
list($INSERT, $FOREIGN) = $this->splitDataForTables($data);
if ($duplicate_check_flag) {
if ($duplicate_check_flag == self::CHECK_DUPLICATE_BASE_TABLE) {
$result = $this->checkDuplicate($INSERT);
}
elseif ($duplicate_check_flag == self::CHECK_DUPLICATE_BASE_AND_FOREIGN_TABLE) {
$result = $this->checkDuplicate($INSERT) || $this->checkDuplcatesForeignTable($FOREIGN);
}
elseif ($duplicate_check_flag == self::CHECK_DUPLICATE_FOREIGN_TABLE) {
$result = $this->checkDuplcatesForeignTable($FOREIGN);
}
if ($result) {
return self::ERR_DUPLICATE;
}
}
$this->_db->beginTransaction();
//dla foreign nie musimy juz przekazywac parametru $duplicate_check_flag
$this->insertForeignTable($INSERT, $FOREIGN);
if (!empty($INSERT)) {
//sprawdz, czy nie ma juz takiego id w bazie jesli zostal okreslony
if(isset($INSERT[$this->_primary]) && $INSERT[$this->_primary]) {
$CHECK_ID = array();
$CHECK_ID[$this->_primary] = $INSERT[$this->_primary];
if ($this->getRowID($CHECK_ID)) {
$last_inserted_id = self::ERR_DUPLICATE;
}
}
$last_inserted_id = parent::insert($INSERT);
}
else {
$last_inserted_id = 0;
}
$this->_db->commit();
}
catch (PDOException $e) {
$this->_db->rollBack();
$last_inserted_id = self::ERR_DUPLICATE;
}
catch (Exception $e) {
$this->_db->rollBack();
$last_inserted_id = self::ERR_DUPLICATE;
}
return $last_inserted_id;
}
/**
* Sprawdza czy podane dane sa juz w bazie danych, opcjonalnie czy naleza do wpisu
* o podanym w drugim parametrze id.
* @param array $DATA
* @param $id - opcjonalnie - id rekordu
* @return (bool)
*/
public function checkDuplicate(array $DATA, $id = 0)
{
if (empty($DATA)) {
return false;
}
$result = $this->getRowID($DATA);
if ($result && $id != $result) {
return true;
}
else {
return false;
}
}
/**
* Pobiera id rekordu dla WHERE tworzonego na podstawie tablicy wejsciowej
* @param array $DATA
* @return (int)
*/
public function getRowID(array $DATA)
{
if (empty($DATA)) {
return false;
}
$WHERE = array();
foreach ($DATA as $var => $value) {
$WHERE[] = $var." = '".$value."'";
}
return $this->_db->fetchOne('SELECT '.$this->_primary.' FROM '.$this->_name.' WHERE '.implode(' AND ', $WHERE));
}
/**
* Zwroc tablice z wszystkimi kolumnami tablicy wywolujacej i tablic powiazanych po kluczu obcym
* @param $id - id rekordu
* @return (array)
*/
public function getRow($id)
{
$select = $this->_db->select()->from(array('base' => $this->_name));
if (!empty($this->_referenceMap)) {
foreach ($this->_referenceMap as $ref => $REF) {
$select->joinLeft(array($ref => $REF['refTableClass']),
'base.'.$REF['columns'][0].' = '.$ref.'.'.$REF['refColumns'][0]);
}
}
$select->where($this->_primary.' = ?', $id);
$stmt = $this->_db->query($select);
return $stmt->fetch();
}
}
Ponieważ jest tutaj użyte rekurencyjne inserte’owanie i update’owanie tabel w metodach, które tworzą i kończą transakcję, musimy zmodyfikować klasę Zend_Db_Adapter_Abstract tak, żeby sprawdzała, czy nie została już wcześniej rozpoczęta transakcja (i nie tworzyła nowej) oraz kończyła transakcję dla ostatniego Commit’a (powiązanego z pierwszym beginTransaction().
Moje rozwiązanie polega na zliczaniu wywołań beginTransaction(), dodaniu zmiennej $is_transaction i zmodyfikowane metody wyglądają następująco:
/**
* Leave autocommit mode and begin a transaction.
*
* @return bool True
*/
public function beginTransaction()
{
$this->is_transaction++;
if ($this->is_transaction > 1) {
return true;
}
$this->_connect();
$q = $this->_profiler->queryStart(‘begin’, Zend_Db_Profiler::TRANSACTION);
$this->_beginTransaction();
$this->_profiler->queryEnd($q);
return true;
}
/**
* Commit a transaction and return to autocommit mode.
*
* @return bool True
*/
public function commit()
{
$this->is_transaction–;
if ($this->is_transaction != 0) {
return true;
}
$this->_connect();
$q = $this->_profiler->queryStart(‘commit’, Zend_Db_Profiler::TRANSACTION);
$this->_commit();
$this->_profiler->queryEnd($q);
$this->is_transaction = false;
return true;
}
Przykład użycia dla danych wygenerowanych we wcześniejszym wpisie (tabele Author, Publisher i Book) – należy pamiętać o zmianie klasy bazowej dla wygenerowanych klas (BaseBook, BaseAuthor i BasePublisher) na Db_Table:
$table = new BaseBook(); $data = array( BaseBook::TITLE => ‘Przygoda tomka’, BaseAuthor::FIRST_NAME => ‘Jan’, BaseAuthor::LAST_NAME => ‘Kowalski’, BasePublisher::NAME => ‘Publicat’ ); $id = $table->insert($data, Db_Table::CHECK_DUPLICATE_BASE_TABLE); $rows = $table->getRow($id); ...
A rollback nie powinien zerować zmiennej is_transaction?
Comment - autor: w. | 1 wrzesień, 2009 |
Zgadza się, w zaproponowanym rozwiązaniu rollback powinien modyfikować zmienną is_transaction lub alternatywnie należy zmienić flow tak, żeby commit zawsze się wykonał (co mi jest bliższe).
W końcu jest okazja update’owac wpis. (Linia 159 i 170)
Comment - autor: zenedith | 1 wrzesień, 2009 |