Zenedith's dev blog

Dałem się namówić.., szatanowi chyba..

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);
...

3 lipiec, 2009 - Opublikował/a zenedith | web | , , | 2 komentarzy

2 komentarzy »

  1. A rollback nie powinien zerować zmiennej is_transaction?

    Comment - autor: w. | 1 wrzesień, 2009 | Odpowiedz

  2. 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 | Odpowiedz


Dodaj komentarz