En effet, je trouve que la plupart des implémentations, en particulier celles qui s'inspirent du pattern "ActiveRecord", sont, certes "sexy" à utiliser pour le développeur, mais sont particulièrement gourmandes en ressources et donc ne sont pas franchement adaptées pour des sites à fort trafic.

Regardons par exemple Doctrine, qui je crois tiens la palme en terme de bloatware dans la catégorie de framework ORM (c'est en tout cas mon avis). Voici un exemple simple. On a un objet Doctrine_table dans $table, qui représente une table dans la base de donnée, et on veut récupérer l'enregistrement dont l'identifiant est $id :

<?php
$record = $table->find($id);
?>

À priori, c'est quelque chose de relativement simple à faire si on le fait "à la main" : construction de la requête SQL dans une chaine, puis on appel l'API de PHP pour interroger la base de donnée, et enfin on stocke les résultats dans un objet. Doctrine fait ça mais en cent fois plus complexe,à cause de l'utilisation du pattern ActiveRecord. Voici le schéma d'exécution de Doctrine à partir de la méthode find :

Doctrine_Table::find    10
   Doctrine_Query_Abstract::From   1
       Doctrine_Query::parseQueryPart  16
           Doctrine_Query::getParser   5
               Doctrine_Query_Part::__construct    1
               Doctrine_Query_From::parse  26
                   Doctrine_Tokenizer::bracketExplode 21 * n
           Doctrine_Hydrate::addQueryPart 6
           ou Doctrine_Hydrate::setQueryPart   3
   Doctrine_Query_Abstract::Where  5
       Doctrine_Query::parseQueryPart  16
           Doctrine_Query::getParser   5
               Doctrine_Query_Part::__construct    1
               Doctrine_Query_Condition::parse  20
                   Doctrine_Tokenizer::bracketExplode 21 * n
                   Doctrine_Tokenizer::bracketTrim 5
           Doctrine_Hydrate::addQueryPart 6
           ou Doctrine_Hydrate::setQueryPart   3
   Doctrine_Hydrate::execute   5 (sans cache des données activé)
       Doctrine_Hydrate::_execute
           Doctrine_Connection::convertBooleans ...
           Doctrine_Hydrate::getQuery   (des dizaines de lignes de codes)
           Doctrine_Hydrate::convertEnums  5
           Doctrine_Hydrate::isLimitSubqueryUsed   1
           Doctrine_Connection::execute ...
       Doctrine_Hydrate::parseData2  (des dizaines de lignes de codes)

C'est la liste partielle des méthodes appelées lors du find. Partielle parce que je n'ai pas pris plus de temps pour calculer le nombre d'itération dans les boucles qu'il y a dans certaines de ces méthodes. Et puis des méthodes comme Doctrine_Hydrate::getQuery qui est chargée de construire la requête SQL, sont énormes (pour ce que ça fait...). Le chiffre est le nombre de ligne de code dans la méthode (ce qui ne veut pas dire que ce soit le nombre de ligne de code executé). Je ne suis pas aller voir non plus en détails ce que fait Doctrine_Connection, parce qu'on ne va plus en finir sinon.

Cependant, même si ce schéma d'exécution est partiel, on se rend tout de même compte que le nombre de lignes de code et le nombre d'appel de méthodes diverses, rien que pour faire un simple SELECT, sont tout simplement faramineux, voir indécent. Et là il ne s'agit que d'un exemple simple. Dés que l'on a un schéma de base de donnée plus complexe avec des jointures un peu partout, c'est pire. Et je n'ai pas encore parlé de tous les traitements exécutés pour avoir notre objet $table. Par exemple, avec Doctrine, il faut créer un objet, basé sur Doctrine_Record, qui représentera un enregistrement (et à partir duquel un objet Doctrine_Table sera instancié pour récupérer ou enregistrer des records).

Voici un exemple :

<?php
class User extends Doctrine_Record
{
   public function setTableDefinition()
   {
       // set 'user' table columns, note that
       // id column is auto-created as no primary key is specified

       $this->hasColumn('name', 'string',30);
       $this->hasColumn('username', 'string',20);
       $this->hasColumn('password', 'string',16);
       $this->hasColumn('created', 'integer',11);
   }
}
?>

Cela veut dire, qu'à chaque requête HTTP, pour chaque objet Doctrine_Record que l'on veut utiliser, le programme redéclare ses champs ! (Je n'ai pas vu de système de cache quelconque dans Doctrine qui éviterait cela).

Au niveau performance, je trouve cela catastrophique, surtout en rapport avec le faible gain pour le développeur en terme d'écriture de code.

Comparons ça maintenant avec jDao. Il lui faut un fichier XML qui décrit la structure d'une table. On peut aussi y déclarer des méthodes qui permettent de réaliser des opérations SQL personnalisées. À parti de ce fichier XML, jDao génère deux classes, qui sont stockées dans un fichier cache, ce qui fait que cette génération n'est faite qu'une seule fois, à la première exécution de l'application. La première de ces classes est la représentation d'un record, avec des propriétés correspondantes au champs de la table. Donc pas de mécanisme compliqué comme il y a derrière le hasColumn de Doctrine. La deuxième classe, contient les méthodes pour réaliser les opérations SQL. Les requêtes de ces méthodes sont partiellement en dur dans le code. Ainsi, l'équivalent du find de Doctrine avec jDao est :

<?php
 $record = $table->get($id);
 ?>

Voici le code de la méthode get() :

   public function get(){
       $args=func_get_args();
       if(count($args)==1 && is_array($args0)){
           $args=$args[0];
       }
       $keys = array_combine($this->getPrimaryKeyNames(),$args );

       if($keys === false){
           throw new jException('jelix~dao.error.keys.missing');
       }

       $q = $this->_selectClause.$this->_fromClause.$this->_whereClause;
       $q .= $this->_getPkWhereClauseForSelect($keys);

       $rs  =  $this->_conn->query ($q);
       $rs->setFetchMode(8,$this->_DaoRecordClassName);
       $record =  $rs->fetch ();
       return $record;
   }

$this->_selectClause.$this->_fromClause.$this->_whereClause sont des chaines contenant des morceaux de requête SQL, générées lors de l'analyse du fichier XML, donc en dur dans la classe. Les autres méthodes sont générées elles aussi et sont très simples. Un exemple :

  public function getPrimaryKeyNames() { return self::$_pkFields;}
  protected function _getPkWhereClauseForSelect($pk){
     extract($pk);
    return ' WHERE  `product_test`.`id`'.'='.intval($id).'';
  }

On est donc loin du bloatware. Sachant que pour récupérer l'objet $table, il ne s'agit que d'un include, son constructeur étant vide.

Finalement le volume de code exécuté par jDao est similaire à ce que l'on ferait à la main en PHP sans ORM. Donc pour faire mieux en terme de performance, c'est difficile ;-)

En effet, je suis parti du constat que pendant la plus grande partie de la vie d'une application, en production, celle-ci ne change pas, les structures de données ne changent pas. Comprendre par là que le schéma de base de donnée ne change pas tous les jours, et que les éventuels changement à chaque nouvelle version peuvent être considérés comme insignifiant vis à vis des milliers, voire des millions de requêtes HTTP que connait l'application. Aussi, on peut considérer que tout le code que Doctrine exécute pour faire ces requêtes, c'est du code totalement inutile : pourquoi redéfinir à chaque requête HTTP les champs à mapper dans les objets record, et reconstruire de A à Z les requêtes SQL alors que le schéma de base ne change pas ? D'un point de vue gestion de ressources, c'est du gâchis énorme. Je trouve que ça n'en vaut tout simplement pas le coup. Et si certains peuvent penser qu'il suffit de rajouter de la mémoire ou changer pour une machine plus puissante afin d'absorber le surcoût en ressource, je trouve que c'est franchement une mauvaise solution en tout point de vue (tant financière qu'environnemental par exemple..)

À propos d'autres ORM, j'ai regardé Propel qui est plus dans l'esprit de jDao, mais est tout de même très volumineux en terme de code même si il est plus puissant que jDao. À mon avis, d'après ce que j'en ai vu au niveau code source, il y a certainement beaucoup d'optimisations à faire dans Propel. J'ai aussi regardé phpMyObject, mais il utilise des principes similaires à Doctrine : il "devine" à chaque utilisation les champs à utiliser, la structure des tables, ce qui en terme de performance n'est vraiment pas bon (d'autant plus qu'à chaque fois que l'on créer soit même un objet record, il fait une requête sur la base de donnée pour connaitre la liste des colonnes, mais on va mettre ce défaut sur la jeunesse du projet, qui aurait besoin d'un bon lifting pour améliorer ses perfs).

Enfin bref, même si il a encore quelques lacunes sur des besoins complexes (que je compte bien combler dans les prochaines versions), jDao est très performant et suffisant dans beaucoup de cas. Certains se plaignent parfois du fait qu'il faille créer un fichier XML, mais je trouve que c'est un faible défaut en regard des ressources que l'on économise par rapport à d'autres ORM. De plus, vu que Jelix propose un script pour le développeur pour générer ce fichier XML à partir d'une table existante, pour moi cet inconvénient n'en est pas un (Et le sera d'autant moins si un jour le plugin JelixEclipse permet d'éditer visuellement un fichier jDao ;-) ).