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.
je vais lire ca…
surement que c’est un cas non prévu pour l’instant dans mon contrôleur CRUD abstract…. surtout les jointures d’une table sur elle même !
Mais ce n’est pas une priorité pour ce que je développe.
En attendant et en avant-première voici les schémas techniques de fonctionnement de mon contrôleur, le code suivra :-)
c’est à l’adresse :
http://www.eozine.fr/temp/CRUD/index.php
bonne lecture !
Merci à toi je vais lire ça moi ausi
A+JYT
Impec ca marche nikel, j’ai une table message qui jointe ma table user pour remonter systematiquement avec le message le login du user qui l’a ecrit. Comme cela
protected $_autojoin = array(
array( ‘table’ => ‘users’,
‘on’ => ‘users.id = messages.user_id’,
‘fields’ => array(’users.login as userName’)
)
);
Sauf que pour le save j’ai un problème :
$message = $messages->fetchRow( »messages.id = « .$id);
$message->readed = 1;
$message->save();
Dans ce cas il me met une erreur : This row has been marked read-only
Donc j’ai testé avec le setReadonly comme cela :
$message = $messages->fetchRow( »messages.id = « .$id);
$message->setReadOnly(false);
$message->readed = 1;
$message->save();
Cette fois j’obtiens le message : SQLSTATE[23000]: Integrity constraint violation: 1052 Column ‘id’ in where clause is ambiguous
Y’a t’il une solution ? je ne vois pas comment faire, merci d’avance.
Bon pour le ambigous j’ai trouvé le souci c’était ma modelisation de BDD qui n’été pas optimum avec les clés primaires toutes nommées id.
En revanche mon problème reste le même si j’ai une auto jointure je ne peux pas save, ou doinsert do update, car lors du refresh celui ci récupère l’objet table jointé et non l’objet table principal. Ici il récupère l’objet table user au lieu de l’objet table message du coup ca bloque au refresh. Une idée pour contourner le problème ?
Tu peux modérer mes 2 précédents messages ca fonctionne grâce au setReadOnly.
J’ai le même problème que tomtom sur le save. Je me retrouve avec un Integrity constraint violation: 1052 Column ‘XXXX’ in where clause is ambiguous
Ce qui semble logique car lors du _refresh sur un save il refait un fetchRow mais sur la clé primaire uniquement sans le nom de la table devans. Etant donné que ma table est jointé sur une autre par la clé primaire de la table il ne sait pas lequel prendre. Comment éviter ce problème facilement ? sekaijin une idée ?
j’utilise des règles de mommage dans mes table. elle très répandue et évitent pas mal de pb de ce genre
entre autre tous les champs propre à une table son préfixé de l’abréviation de la table par exemple user(usr_id, usr_name, …)
mais j’ai effectivement remonté à zend le pb de fetchRow qui ne préfixe pas le champs.
Je ne l’ai pas avec moi mais j’ai sur une autre machine une solution pour corriger ce problème.
A+JYT
J’ai retrouvé le code en question.
ouvrez la classe Zend_Db_Table_Abstract copiez la méthode « find »
Ajoutez cette méthode à votre classe et remplacez
par
votre classe préfixera alors les clefs primaires de la table.
A+JYT
Franchement nickel tout ça! J’ai aussi opter pour les préfixes sur les champs de toutes mes tables.
Par contre j’ai personnellement modifier la méthode info() ainsi pour qu’elle respecte la signature de Zend_Db_Table_Abstract
/**
* Returns table information.
*
* @return array
*/
public function info($key = null)
{
$info = parent::info();
$info[self::FAST_RESTRICT] = $this->_restrict;
$info[self::FAST_AUTOJOIN] = $this->_autojoin;
if ($key === null) {
return $info;
}
if (!array_key_exists($key, $info)) {
require_once 'Fast/Db/Table/Exception.php';
throw new Fast_Db_Table_Exception('There is no table information for the key "' . $key . '"');
}
return $info[$key];
}
Ca évite d’avoir une erreur ou un warning je sais plus trop :)
En tout cas merci JY !
Effectivement je ne relate dans ce poste qu’une expérimentation
Merci de nous faire part de ces éléments complémentaires
A+JYT
C’est quoi l’intérêt/la différence par rapport aux $_referenceMap et $_dependentTables que propose le ZF en native ?
$_referenceMap est un tableau qui contient les définition des clef externe de ta table.
chaque entrée dans le tableau et référencé par le nom de la table externe
sa valeur est un tableau définissant la relation columns la liste des colonnes locale entrant dans la relation (array) ‘refColumns’ la liste des colonnes externe entrant dans la relation (array)
‘refTableClass’ nom de la classe associé à la table externe
par exemple la table person qui contient usr_addr une clef externe addr_id de la table address
$_referenceMap = array(
‘address’ => array (
‘columns’ => array(’usr_addr’),
‘refColumns’ => array(’addr_id’),
‘refTableClass’ => array(’Model_Address_Table’)
)
);
nous avons donc défini dans la classe Model_Person_Table associé à la table person la référence externe la table address associé à la classe Model_Address_Table
dans l’autre sens la table adresse est elle aussi en relation avec la table person (toute les personne ayant cette adresse)
dans la classe Model_Address_Table on va donc définir $_dependentTables qui est une table contenant la liste des classes associé à des table qui on une clef externe d’address
$_dependentTables= array(’Model_Person_Table’);
ZF fournit alors les méthodes nécessaire pour invoquer la relation dans un sens ou dans l’autre
A+JYT
Mais est que ton auto jointure ne fait pas doublon ? car j’ai l’impression que ca fait pareil que $_referenceMap et $_dependentTables.
Non la différence c’est que lorsque tu demande un élément dans une table Zend tu obtient un enregistrement de cette table. si tu veux un élément join Zend te fournis une méthode qui va faire une requête pour l’obtenir
si tu a par exemple un dossier qui change d’état et que tu a une table des états lorsque tu veux afficher le contenu de la table avec son état tu dois faire une requête pour récupérer tous les dossier puis autant de requête pour récupérer pour chaque dossier son état
tu peux optimiser en chargeant d’abord tous les états mais alors il te faut parcourir tout le resultset des dossier pour leur associer l’état.
alors qu’en SQL tu fais une seule requête avec une jointure et tu obtiens le tout.
Zend te propose alors d’utiliser Zend_Db_Select. mais alors tu n’a plus à faire à une table avec ses mécanismes.
si tu veux que ta classe associé à ta table te remonte le résultat de la jointure et se comporte comme une classe de table Zend il te faut redéfinir la requête associée à la table et quelques autres éléments. ce que je propose là est une méthode pour ne pas avoir à redéfinir soit-même la requête mais juste de la déclarer.
ainsi lorsque tu pioche un ou plusieurs éléments dans la table dossier tu obtiens un dossier qualifié de son état tu conserve le fonctionnement d’une Zend_Db_Table mais tous les objets qui en sortent sont issue de la requête avec jointure.
A+JYT
Bonjour, merci pour le partage ! ( et pour les autres articles très intéressant ! )
Par contre j’essaye de mettre en place l’autojointure avec la version 1.6 du framework et cela ne semble pas fonctionner.
je n’ai fait aucun test avec la 1.6
J’ai le nez dans le guidon et je n’ai pas de temps pour regarder les dernières innovation de ZF
Je suis désolé. Mais je ne pourrais pas me pencher dessus avant très longtemps
A+JYT
Je suis désolé pour la personne qui me faisait part de sont expérience avec la version 1.7 j’ai par erreur modéré son commentaire en indésirable et je ne sais comment revenir en arrière.
:(
A+JYT
C’est simple, je refais le commentaire … :)
Surcharger assemble() au lieu de __toString() dans Fast_Db_Table_Select.
merci