Archive pour février 2008

Auto-Jointure

Mardi 12 février 2008

Dans mon article « Ajouter un champ calculé dans une table. » Je parlais d’une méthode pour ajouter des champs dans les objets row issue d’une table qui ne les contient pas base.
La version 1.5 de ZF a introduit une nouvelle classe qui facilite ce genre de manipulation.

L’Auto Jointure simple

Cette petite nouveauté introduit de façon claire les select qui sous-tend la classe Zend_Db_Table. Pour voir comment cette introduction change la donne je me suis posé le problème de l’auto jointure. J’ai une table principale et une où plusieurs table de références.
Le but de l’autojointure et de remonter avec les éléments de la table les valeurs de références. Par exemple les éléments de ma table principale sont typés, il contient un id sur la table de référence des types. Je veux lorsque je lis un enregistrement pouvoir remonter le label de sont type sans avoir systématiquement à faire une deuxième requête.
Les données complémentaires étant des données de référence leur valeur ne change pas (pas lorsqu’on manipule un élément de la table principale) je ne poserais donc pas ici le problème de l’écriture en base.

Zend_Db_Table_Select

Voyons comment fonction les classes de Zend_Db lorsque nous lisons dans la table. L’appel des méthodes fetch, fetchCols, fetchRow, fetchAll, et find font tous appels à _fetch. Dans les versions antérieures à la 1.5 la méthode _fetch recevais les paramètres de la requête. Where et autres order ou limit. La méthode _fetch fabriquait donc un select avec $this->db->select() et y appliquait les clauses adéquates.
Avec la version 1.5 les fonctions suscitées vont-elles-même créer l’objet select et lui ajouter les clauses nécessaires. Pour éviter que chaque fonction réinvente la roue elles ne vont pas créer un simple select mais un Zend_Db_Table_Select dont la base est simplement
Select * From tablename. Pour cela une nouvelle méthode à été ajouté à Zend_Db_Table : select il est donc possible de demander à la table un select préconfiguré pour le manipuler à sa sauce. Nous allons donc nous arranger pour que ce select ne soit pas un simple Zend_Db_Table_Select mais pour qu’il ajoute automatiquement une ou plusieurs jointures à la table.

Fast_Db_Table

Une classe table acceptant l’autojointure. Notre but est de définir une classe comme Zend_Db_Table_Abstract que nous pourrons dériver pour la mapper sur les éléments de la base. On va donc introduire un membre qui listera les jointures à effectuer dans les requêtes. Et tant que nous y sommes nous allons ajouter des restrictions automatique (clauses where ajouté systématiquement)

 /**
 * Definition de base d'une table Fast
 * elle étend la classe Zend_Table et lui adjoint un classe spécifique pour les enregistrement
 * ainsi que les méthode courantes d'accès au données
 *
 * @see  Zend_Db_Table
 * @see  Fast_Exception_Db
 * @see  Fast_Db_Row
 * @author Jean-Yves Terrien
 */
Class Fast_Db_Table extends Zend_Db_Table_Abstract {

   const FAST_RESTRICT = 'fast_restrict';
   const FAST_AUTOJOIN = 'fast_autojoin';
   /**
   * Classname for select , Zend_Db_Table_Select,...
   *
   * @var string
   */
   protected $_selectClass = 'Fast_Db_Table_Select';
   /**
   * Restriction for query
   *
   * @var string
   */
   protected $_restrict = null;
   /**
   * Auto Joined table for query
   *
   * @var string
   */
   protected $_autojoin = NULL;
   /**
    * Returns an instance of a Zend_Db_Table_Select object.
    *
    * @return Zend_Db_Table_Select
    */
   public function select()
   {
      if ('Zend_Db_Table_Select' == $this->_selectClass) {
         $select = parent::select();
      } else {
         Zend_Loader::loadClass($this->_selectClass);
         $select = new $this->_selectClass($this);
      }
      return $select;
   }
    /**
     * Returns table information.
     *
     * @return array
     */
    public function info()
    {
        $info = parent::info();
        $info[self::FAST_RESTRICT] = $this->_restrict;
        $info[self::FAST_AUTOJOIN] = $this->_autojoin;
        return $info;
    }
}

Voilà la base de notre classe on va lui indiquer la classe Select à utiliser (il faudra qu’elle dérive de Zend_Db_Table_Select) cela permettra de la surcharger. On redéfinit la méthode Select() pour tenir compte de notre classe Select et on redéfini la méthode info pour que la classes Select connaisse les jointures à faire.
La définition de la requête étant faite dans la classe Select c’est tout pour la classe Table.

Fast_Db_Table_Select

Un select pour table à auto jointure.
Il est un peu étonnant de voir que la classe Zend_Db_Table_Select ne crée pas la partie from de la table par défaut. Elle ne le fait qu’au moment de la transformation en chaine. Du coup si on tente de faire le join avant on obtient une exception. Il faut donc respecter cette mécanique pour nous insérer au mieux dans ZF

 class Fast_Db_Table_Select extends Zend_Db_Table_Select
{
   private $_autojoined = false;
   private $_useRestrict = true;
   /**
   * Performs a validation on the select query before passing back to the parent class.
   * Ensures that only columns from the primary Zend_Db_Table are returned in the result.
   *
   * @return string This object as a SELECT string.
   */
   public function __toString()
   {
      if (!$this->_autojoined) {
         $this->_autojoined = true;

         $fields  = $this->getPart(Zend_Db_Table_Select::COLUMNS);
         $primary = $this->_info[Zend_Db_Table_Abstract::NAME];
         $schema  = $this->_info[Zend_Db_Table_Abstract::SCHEMA];

         // If no fields are specified we assume all fields from primary table
         if (!count($fields)) {
            $this->from($primary, '*', $schema);
            $fields = $this->getPart(Zend_Db_Table_Select::COLUMNS);
         }

         if ($this->_useRestrict) {
            if (is_string($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
               $restricts[] = $this->_info[Fast_Db_Table::FAST_RESTRICT];
            } elseif (is_array($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
               $restricts = $this->_info[Fast_Db_Table::FAST_RESTRICT];
            } else {
               $restricts = array();
            }
            foreach ($restricts as $restrict) {
               $this->where($restrict);
            }
         }
         if (is_array($this->_info[Fast_Db_Table::FAST_AUTOJOIN])) {
            $this->setIntegrityCheck(false);
            foreach ($this->_info[Fast_Db_Table::FAST_AUTOJOIN] as $join) {
               if (is_array($join)) {
                  $this->join($join['table'], $join['on'], $join['fields']);
               }
            }
         }
      }
      return parent::__toString();
   }
   /**
      @function setRestrict()
   	@param boolean $restrict Use restriction for this select
      @return Fast_Db_Table_Select Description
   */
   function setRestrict($restrict) {
      $this->_useRestrict = $restrict;
      return $this;
   } // end function setRestrict
}

Le premier membre private $_autojoined indique que la jointure à déjà été faite. Vu que nous l’ajoutons lors de la transformation en chaine il ne faudrait pas que le soit plusieurs fois. Un simple echo sur l’objet appelle __toString()
Le second private $_useRestrict = true indique que nous devons ou pas utiliser les clauses restrictives
La méthode setRestrict permet de changer ce mode.
La méthode __toString est semblable à celle de sa classe parente elle ne fait que parcourir le tableau d’auto jointure pour ajouter les clauses join.

Une utilisation

La classe user. Nous allons remonter le champ label du profil en même temps que l’utilisateur.

 Class Adm_Model_User_Table extends Fast_Db_Table {
   /**
   * The table name.
   *
   * @var array
   */
   protected $_name = 'user';

   /**
   * Classname for row
   *
   * @var string
   */
   protected $_rowClass = 'Adm_Model_User_Row';
   /**
   * Auto Joined table for query
   *
   * @var string
   */
   protected $_autojoin = array(
      array('table' => 'profile',
            'on' =>  'profile.prf_id = user.prf_id',
            'fields' => array('prf_label')
      )
   );
   /**
   * Restriction for query
   *
   * @var string
   */
   protected $_restrict = array('profile.prf_valid = 1');
}

Lorsque nous utiliserons cette classe la requête générée par défaut sera

Select
   user.*,
   profile.prf_label
FROM user
INNER JOIN profile ON (profile.prf_id = user.prf_id)
WHERE profile.prf_valid = 1

Conclusion

Zend_Db_Table_Select apporte une ouverture nouvelle pour adapter au mieux à ses besoins le mapping objet de ZF. Les classes ci-dessus ne sont que des premières versions. Elles sont grandement améliorables. Par exemple pour gérer les jointures plus complexes comme left rigth mais aussi avec des clauses de recoupement etc.

Ajouter un champ calculé dans une table.

Vendredi 1 février 2008

Il arrive parfois qu’il soit intéressant de véhiculer un champ dans un objet de mapping qui n’est pas conservé en base. C’est le cas entre autre des champs calculés. Je veux par exemple un objet facture. Lorsque je manipule ma facture un élément important est le total de la facture. Mais une facture en elle-même est composée de champs qui lui sont propres et de lignes de facturations. Chaque ligne véhicule une partie du total de la facture. Lorsque je manipule la facture je n’ai pas nécessairement besoin de ses lignes de facturations. Par exemple lorsque je vérifie que le montant payé et bien le bon. Inutile de remonter toutes les lignes seul le total m’intéresse. Je peux alors décider de garder en base le total. Il me faudra alors veiller à ce que ce total soit tenu à jour en adéquation avec mes lignes de facturations. Je peux aussi décider de ne pas le garder en base. Mais alors il me faudra le calculer et donc faire deux accès à la base pour obtenir ma facture (sans ses lignes mais avec) son total

Ajouter un champ à un objet de mapping

Une solution consiste à calculer ce champ lors de la lecture en base et de le garder dans un coin. Si je le mets directement dans mon objet de mapping je vais me heurter à quelques difficultés. Par exemple si j’enregistre cet objet l’ORM de Zend va au mieux supprimer le champ, au pire lever une exception et il aura raison. Il ne sait qu’en faire. Le but de cet article est de voir les points à lever pour arriver à cette solution.

la table facture

La toute première étape consiste à crée un classe pour la table facture

Class Model_Facture_Table extends Zend_Db_Table_Abstract {
   protected $_name = 'facture';
   protected $_rowClass = ' Model_Facture_Row';
   public function __construct($config = array())
   {
      parent::__construct($config);
	$this->_cols[] = 'fac_total';
   }
}

Et d’y ajouter une colonne. Ainsi lorsque je sortirais ou entrerait une facture dans ma table le champ fac_total ne sera pas inconnu.
Mais il va falloir aller un peu plus loin si on ne veut pas se retrouver avec des Exception de partout. La première étape passe par le calcul de ce champ. Donc lorsqu’on lit un enregistrement dans la base. ZF est ainsi fait que quelque soit la façon dont vous interrogez votre table il passe toujours par la même méthode. Sauf évidement si vous écrivez vous-même une requête. La méthode qui définit la requête à effectuer sur la base pour lire un ou plusieurs enregistrements s’appelle _fetch. Il nous faut donc la modifier pour obtenir le résultat que nous cherchons. Ainsi toute lecture prendra en compte notre modification. Pour bien comprendre ce que fait cette méthode il suffit de se pencher sur le fonctionnement d’une Zend_Db_Table. De façon générale une Zend_Db_Table c’est

SELECT * FROM tableName ;

Les autres méthodes de recherche ne font qu’ajouter des clauses WHERE ORDER ETC. le but de la méthode _fetch est de construire cette requête.
Moi, je voudrais à la place

SELECT
   facture .*,
   SUM(lig_prix*lig_qte) AS fac_total
FROM facture
INNER JOIN lignes USING (fac_id)
GROUP BY fac_id;

modifier la méthode _fetch

Tout d’abord voyons comment est faite la méthode de Zend

    /**
     * Support method for fetching rows.
     *
     * @param  string|array $where  OPTIONAL An SQL WHERE clause.
     * @param  string|array $order  OPTIONAL An SQL ORDER clause.
     * @param  int          $count  OPTIONAL An SQL LIMIT count.
     * @param  int          $offset OPTIONAL An SQL LIMIT offset.
     * @return array The row results, in FETCH_ASSOC mode.
     */
    protected function _fetch($where = null,
                              $order = null,
                              $count = null,
                              $offset = null)
    {
        // selection tool
        $select = $this->_db->select();

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

        // 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;
    }

Cette méthode est, un peu, longue mais au final par très complexe. On voit vite que notre SELECT * est à la ligne

$select->from($this->_name, $this->_cols, $this->_schema);

Et que c’est là qu’il faut intervenir. En effet le reste n’est que l’ajout de clause diverse.
Remplaçons donc

$cols = $this->_cols;
unset($cols[array_search('fac_total',$cols)]);
$select->from($this->_name, $cols, $this->_schema)
->join('lignes',’ligne.fac_id = facture.fac_id’, array('fac_total' =>Zend_Db_Exp('SUM(lig_prix * lig_qte)')))
->group('fac_id');

Nous avons maintenant une table qui lit des factures avec leur total.

L’écriture

Tant que nous ne faisons que lire dans la table avec cet objet, nous n’auront pas de problème.
Mais si nous tentons un update ou un insert nous allons avoir un problème. En effet nous allons essayer de mettre à jour dans la base un champ qui n’y est pas. Il nous faut donc retirer ce champ de l’objet. Avant l’enregistrement.

   public function insert(array $data) {
      unset($data[‘fac_total’]);
      parent::insert($data);
   }

Cela n’est en fait pas bien compliqué. La méthode insert va en accord avec la liste des colonnes de la table tenter d’ajouter l’enregistrement avec tous les champs présents dans $this->_cols si un champ de data n’est pas dans la liste, il sera supprimé, mais fac_total y est puisque nous l’avons ajouté. Mais ce champ n’est pas dans la table le moteur SQL va donc rejeter la requête. Il suffit donc de supprimer ce champ des données.
La méthode update à un comportement équivalent mais légèrement différent. En ce sens que update va tenter de mettre à jour même les champs qui ne son pas présent dans $data. Si je retire simplement le champ fac_total la méthode update tentera de le mettre à null dans la base. Il faut donc retirer le champ des données mais aussi de la liste des colonnes. Et le restituer ensuite car sinon notre objet table sera incohérent.

    public function update(array $data, $where)
    {
      unset($this->_cols[array_search(‘fac_total’,$this->_cols)]);
      unset($data[‘fac_total’]);
      $res = parent::update($data, $where);
      $this->_cols[] = ‘fac_total’;
      return $res;
    }

Il est ainsi possible d’ajouter de nombreux champs dans un objet table qui ne serons pas stockés en base. Il est assez simple de généraliser cette méthode. En se basant sur la description des relations dans les Zend_Db_Table on peut imaginer une classe abstraite qui contiendrait un tableau des autoJoinnedTable et qui implémenterait ce principe.

Un exemple généralisé

Pour ma part je l’utilise dans un tout autre contexte. J’utilise des tables hiérarchiques en POO il est simple et efficace d’utiliser une référence sur l’objet parent pour constituer une hiérarchie. Mais cette approche n’est pas performante dans une base de données. Une représentation intervallaire l’est bien mieux. Je vous conseille de lire les articles sur le sujet.
Dans le cas qui m’intéresse, j’ais donc des tables qui on un id numérique unique comme clef et des données. Le principe de la représentation intervallaire consiste à ajouter une borne droite et une borne gauche, j’ai aussi très souvent besoin de niveau hiérarchique relatif. Par exemple tous les nœuds au rand N+1, N+2 et N+3 d’un nœud donné. J’ajoute donc dans ma table un champ level.
Mais du côté PHP il est plus simple d’utiliser une relation père fils que la notion d’intervalle. Ma classe va donc masquer la représentation intervallaire à PHP. Elle va gérer elle-même les transactions nécessaires pour maintenir à jours les bornes des éléments de la table.

Zend_Loader::loadClass('Zend_Db_Table');

Class Fast_Db_Hierarchical extends Zend _Db_Table {

   /**
   * left field name in table
   *
   * @var string
   */
   protected $_left = NULL;

   /**
   * right field name in table
   *
   * @var string
   */
   protected $_right = NULL;

   /**
   * level field name in table
   *
   * @var string
   */
   protected $_level = NULL;

   /**
   * virtual field name used has id of parent
   *
   * @var string
   */
   protected $_parent = NULL;

   public function __construct($config = array())
   {
      parent::__construct($config);
      if (null == $this->_left)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEFT_KEY);
      if (null == $this->_right)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_RIGHT_KEY);
      if (null == $this->_level)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEVEL_KEY);
      if (null == $this->_parent)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_PARENT);
      $this->_cols[] = $this->_parent;
   }

   public function getById($id) {
      $rows = $this->find($id);
      if ($rows) {
         return $rows->current();
      }
      return false;
   }

   public function deleteById($id) {
      if ($id == 1) return false; // on ne peut supprimer la racine

      $this->_db->beginTransaction();
      $parent = $this->_db->select();
      $parent->from($this->_name, array('delete_left' => $this->_left,
                                        'delete_right' => $this->_right))
             ->where($this->_primary[1].' = :_deleteId');
      $statement = $this->_db->prepare($parent);
      $statement->execute(array('_deleteId' => $id));
      list($deleteLeft, $deleteRight) = array_values($statement->fetch());
      $res = false;
      if ($deleteLeft) {
         $row = $this->getById($id);
         $res = $row->delete();

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_left.' = '.$this->_left.' - 1
                WHERE '.$this->_left.' >= '.$deleteLeft.'
                AND '.$this->_right.' < '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_left.' = '.$this->_left.' - 2
                WHERE '.$this->_left.' >= '.$deleteLeft.'
                AND '.$this->_right.' > '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_right.' = '.$this->_right.' - 1
                WHERE '.$this->_right.' >= '.$deleteLeft.'
                AND '.$this->_right.' < '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_right.' = '.$this->_right.' - 2
                WHERE '.$this->_right.' >= '.$deleteLeft.'
                AND '.$this->_right.' > '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $this->_db->commit();
         } else {
            $this->_db->rollback();
         }
      }
      return $res;
   }

   public function UpdateById($data) {
      // on ne peut mettre à jour les donnée hiérarchique
      // ie on ne peut déplacer un noeud dans l'arbre.
      unset($data[$this->_parent]); // ne fait pas partie de la table
      unset($data[$this->_left]);   //ne peut être changé
      unset($data[$this->_right]);  //ne peut être changé
      unset($data[$this->_level]);  //ne peut être changé
      $res =  parent::UpdateById($data);
      return $res;
   }

	public function insert(array $data) {
      # select left and level of parent
      $parentId = $data[$this->_parent];

      $this->_db->beginTransaction();
      $parent = $this->_db->select();
      if (null != $this->_level) {
         $fields = array('parent_left' => $this->_left,
                         'parent_level' => $this->_level);
      } else {
         $fields = array('parent_left' => $this->_left,);
      }

      $parent->from($this->_name, $fields)
             ->where($this->_primary[1].' = :_parentId');
      $statement = $this->_db->prepare($parent);
      $statement->execute(array('_parentId' => $parentId));
      list($parentLeft, $parentLevel) = array_values($statement->fetch());

      $res = false;
      if ($parentLeft) {
         #update tree
         $statement = $this->_db->prepare(
            'UPDATE '.$this->_name.'
             SET '.$this->_left.' = '.$this->_left.' + 2
             WHERE '.$this->_left.' > '.$parentLeft.';');
         $res = $statement->execute();
         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_right.' = '.$this->_right.' + 2
                WHERE '.$this->_right.' > '.$parentLeft.';');
            $statement->execute();
         }

         #insert node
         if ($res) {
            unset($data[$this->_parent]);
            $data[$this->_left] = $parentLeft + 1;
            $data[$this->_right] = $parentLeft + 2;
            if (null != $this->_level)
               $data[$this->_level] = $parentLevel + 1;
            $res =  parent::insert($data);
   		}
         if ($res) {
            $this->_db->commit();
         } else {
            $this->_db->rollback();
         }
      }
      return $res;
	}

    /**
     * Support method for fetching rows.
     *
     * @param  string|array $where  OPTIONAL An SQL WHERE clause.
     * @param  string|array $order  OPTIONAL An SQL ORDER clause.
     * @param  int          $count  OPTIONAL An SQL LIMIT count.
     * @param  int          $offset OPTIONAL An SQL LIMIT offset.
     * @return array The row results, in FETCH_ASSOC mode.
     */
    protected function _fetch($where = null, $order = null, $count = null, $offset = null)
    {
        // selection tool
        $select = $this->_db->select();

        //no _parent col on master table
        $cols = $this->_cols;
        unset($cols[array_search($this->_parent,$cols)]);

        // the FROM clause
        $select->from($this->_name, $cols, $this->_schema);
        // add the parent col
        $select->join(array('parent' => $this->_name),
                      '(parent.'.$this->_left.' < workgroup.'.$this->_left.') AND
                       (parent.'.$this->_right.' > workgroup.'.$this->_right.') AND
                       (parent.'.$this->_level.' = workgroup.'.$this->_level.' -1)',
                      array('parent_id' => 'parent.'.$this->_primary[1].''));

        // 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;
    }

    public function update(array $data, $where)
    {
      unset($this->_cols[array_search($this->_parent,$this->_cols)]);
      unset($data[$this->_parent]);
      $res = parent::update($data, $where);
      $this->_cols[] = $this->_parent;
      return $res;
    }

   protected function _parent($row, $fiels) {
      $parent = $this->_parents($row, $fiels)
             ->order($this->_right)
             ->limit(1);
      return $parent;
   }
   protected function _parents($row, $fiels) {
      $parent = $this->_db->select();
      $parent->from($this->_name, $fiels)
             ->where($this->_left.'  < '.$row->{$this->_left})
             ->where($this->_right.' > '.$row->{$this->_right});
      return $parent;
   }
   protected function _childs($row, $fiels) {
      $childs = $this->_db->select();
      $childs->from($this->_name, $fiels)
             ->where($this->_left.'  > '.$row->{$this->_left})
             ->where($this->_right.' < '.$row->{$this->_right});
      return $childs;
   }

    /**
     * This is the find Zend_Db_Table Abstract method
     * But the where closes are prefixed by the table name
     *
     * Fetches rows by primary key.
     * The arguments specify the primary key values.
     * If the table has a multi-column primary key, you must
     * pass as many arguments as the count of column in the
     * primary key.
     *
     * To find multiple rows by primary key, the argument
     * should be an array.  If the table has a multi-column
     * primary key, all arguments must be arrays with the
     * same number of elements.
     *
     * The find() method always returns a Rowset object,
     * even if only one row was found.
     *
     * @param  mixed                         The value(s) of the primary key.
     * @return Zend_Db_Table_Rowset_Abstract Row(s) matching the criteria.
     * @throws Zend_Db_Table_Exception
     */
    public function find()
    {
        $args = func_get_args();
        $keyNames = array_values((array) $this->_primary);

        if (empty($args)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("No value(s) specified for the primary key");
        }

        if (count($args) != count($keyNames)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
        }

        $whereList = array();
        $numberTerms = 0;
        foreach ($args as $keyPosition => $keyValues) {
            // Coerce the values to an array.
            // Don't simply typecast to array, because the values
            // might be Zend_Db_Expr objects.
            if (!is_array($keyValues)) {
                $keyValues = array($keyValues);
            }
            if ($numberTerms == 0) {
                $numberTerms = count($keyValues);
            } else if (count($keyValues) != $numberTerms) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
            }
            for ($i = 0; $i < count($keyValues); ++$i) {
                $whereList[$i][$keyPosition] = $keyValues[$i];
            }
        }
        $whereClause = null;
        if (count($whereList)) {
            $whereOrTerms = array();
            foreach ($whereList as $keyValueSets) {
                $whereAndTerms = array();
                foreach ($keyValueSets as $keyPosition => $keyValue) {
                    $whereAndTerms[] = $this->_db->quoteInto(
                        $this->_db->quoteIdentifier($this->_name).'.'
                       .$this->_db->quoteIdentifier($keyNames[$keyPosition], true) . ' = ?',
                        $keyValue
                    );
                }
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
            }
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
        }

        return $this->fetchAll($whereClause);
    }

}

Vous aurrez noté la présence de la méthode find alors qu’elle est disponible dans la classe Zend_Db_Table. Cela vient du fait que je fais une auto-jointure je joins la table sur elle-même. Du coup tous les champs de la table sont en double dans la requête. Or la méthode find construit des closes where simple. Il est nécessaire dans ce cas de les préfixer du nom de la table c’est ce que j’ai ajouté à la méthode find.

Conclusion

Cette façon de dériver la classe Zend_Db_Table permet d’imaginer toute sorte de mapping entre un objet et un ensemble de tables dans la base. Par exemple un modèle ou les adresses sont dans une table à part des clients alors que l’objet de mapping remonte toujours l’ensemble. Ou la remonté systématique des valeurs des tables de références etc.

A+JYT
ZIP File : Hierarchical Table