Avertissement, 03 dec 2007 : Je viens de remarquer que l'extension xdebug me donne des résultats qui ne permettent pas d'arriver aux mêmes conclusions qu'avec un simple calcul fait avec un microtime(). Aussi les résultats montrés dans ce billet et les conclusions ne sont pas bonnes. Je publierais de nouveau résultats sans xdebug

Disclaimer, 03 dec 2007 : I just discover that I cannot have same conclusions between results given by the xdebug extension (or when it is just activated) and results given by some simple microtime() (with xdebug deactivated). So following results and conclusions are wrong. I will publish new results without xdebug...

Petit rappel sur les frameworks ORM

Ce sont des outils permettant de manipuler les données d'une base au travers d'objets qui représentent généralement des enregistrements de tables. Ils évitent d'utiliser du SQL, et facilite donc la sélection, l'enregistrement ou la destruction des données. En fait, les frameworks ORM ont, en théorie, pour objectif de mimer les bases de données objets (hélas peu courantes sur le marché), en s'appuyant sur une base de donnée relationnelle.

Conditions des tests

Les tests ont été effectué sur une machine ayant un processeur Athlon 2600+, avec 512Mo de RAM et sur laquelle est installé un système Kubuntu 6.10. Les quatre frameworks ORM ont été testé avec la même base de donnée, qui contient une table city (que j'ai récupéré quelque part sur le web, et contenant 36 682 communes françaises) et une table departement (contenant les 96 departements français). En voici le schéma :

CREATE TABLE `city` (
 `id` int(11) NOT NULL auto_increment,
 `name` varchar(200) NOT NULL default '',
 `region` varchar(2) NOT NULL default '',
 `subregion` varchar(200) NOT NULL default '',
 `postcode` varchar(8) NOT NULL default '',
  PRIMARY KEY  (`id`),
  KEY `region` (`region`)
);

CREATE TABLE `departement` (
 `code` varchar(2) NOT NULL,
 `nom` varchar(50) NOT NULL,
 PRIMARY KEY  (`code`)
);

Le champs region de la table city est une clé étrangère vers la table departement.

Les logiciels utilisés sont ceux fournis dans la distribution Kubuntu : Mysql 5.0.24a, Apache 2.0.55 et PHP 5.1.6. Les frameworks ORM testés sont tous les dernières versions stables disponibles il y a presque trois semaines (les tests ont été fait durant le week-end du 11 novembre, désolé pour ce compte rendu tardif ;-)) : jDao livré dans Jelix 1.0beta3.1, Doctrine 1.0 beta 1, Propel 1.2.1, et phpMyObject 0.10.

Préparations des scripts de tests

L'installation des frameworks ORM est une installation standard, avec la configuration d'origine, donc sans "tuning" effectué. Seule exception : j'ai eu pitié de Doctrine, et j'ai donc utilisé la version "compilé", qui est en fait un fichier de 496ko (jDb+jDao ne fait que 81ko) contenant tout le code source de Doctrine, évitant les nombreux accès disque durant un test (m'enfin bon, vu les résultats, je ne pense pas que ça change grand chose).

Afin de lancer les tests, il faut écrire le code qui va permettre aux frameworks de connaitre le schéma de base de donnée.

Pour jDao

Il est intégré au framework Jelix (pas de version "standalone" disponible). J'ai donc réalisé un module avec un contrôleur, pour lancer les tests. Pour jDao, j'ai crée trois fichiers DAO pour les deux tables :

city.dao.xml

 <dao xmlns="http://jelix.org/ns/dao/1.0">
   <datasources> <primarytable name="city" realname="city" primarykey="id" /> </datasources>
   <record>
       <property name="id"  datatype="autoincrement"/> 
       <property name="name" datatype="string" required="true"/>
       <property name="region"  datatype="string" required="true"/>
       <property name="subregion" datatype="string" required="true"/>
       <property name="postcode" datatype="string" required="true"/>
   </record>
   <factory>
       <method name="getByLimit" type="select"> <parameter name="nb" /><limit offset="0" count="$nb"/></method>
   </factory>
</dao>

departement.dao.xml

<dao xmlns="http://jelix.org/ns/dao/1.0">
   <datasources>   <primarytable name="departement" realname="departement" primarykey="code" />  </datasources>
   <record>
       <property name="code" datatype="string" required="true"/>
       <property name="nom" datatype="string" required="true"/>
   </record>
</dao>

citydep.dao.xml : c'est un objet jdao spécifique pour récupérer à la fois une ville et son département associé.

<dao xmlns="http://jelix.org/ns/dao/1.0">
   <datasources>
       <primarytable name="city" realname="city" primarykey="id" />
       <foreigntable name="departement" realname="departement" primarykey="code" onforeignkey="region" />
   </datasources>
   <record>
       <property name="id"  datatype="autoincrement"/>
       <property name="name" datatype="string" required="true"/>
       <property name="region"  datatype="string" required="true"/>
       <property name="subregion" datatype="string" required="true"/>
       <property name="postcode" datatype="string" required="true"/>
       <property name="departement" fieldname="nom" table="departement" datatype="string" />
   </record>
   <factory>
       <method name="getByLimit" type="select"> <parameter name="nb" /> <limit offset="0" count="$nb"/> </method>
   </factory>
</dao>

Ces fichiers sont au premier lancement, transformés par jDao en classes PHP et stockées dans des fichiers de cache. Ces derniers seront alors chargé les fois suivantes comme des fichiers de classes classiques.

Pour Doctrine

Il faut écrire des classes décrivant le schema. Les voici pour les tests :

class city extends Doctrine_Record {
   public function setTableDefinition() {
       $this->hasColumn('id', 'integer',11, array('notnull' => true, 'primary' => true, 'unsigned' > true, 'autoincrement' => true));
       $this->hasColumn('name', 'string',200, array('notnull' => true));
       $this->hasColumn('region', 'string',2, array('notnull' => true));
       $this->hasColumn('subregion', 'string',200, array('notnull' => true));
       $this->hasColumn('postcode', 'string',8, array('notnull' => true));
   }
   public function setUp() {
       $this->hasOne('departement', array('local' => 'region', 'foreign' => 'code'));
   }
}
class departement extends Doctrine_Record {
   public function setTableDefinition() {
       $this->hasColumn('code', 'string',2, array('notnull' => true, 'primary' => true));
       $this->hasColumn('nom', 'string',50, array('notnull' => true));
   }
   public function setUp() {
       // notice the 'as' keyword here
       $this->hasMany('city', array('local' => 'code','foreign' => 'region'));
   }
}

Pour Propel

Comme jDao, le schema doit étre déclaré dans un fichier XML que voici :

 <database name="insee" defaultIdMethod="native">
   <table name="city" description="City Table">
       <column name="id" type="integer" primaryKey="true" autoIncrement="true" required="true" description="Id"/>
       <column name="name" type="varchar" size="200" required="true" description="name"/>
       <column name="region" type="varchar" size="2" required="true" description="region foreign key"/>
       <column name="subregion" type="varchar" size="200" required="true" description=""/>
       <column name="postcode" type="varchar" size="8" required="true" description=""/>
       <foreign-key foreignTable="departement">
           <reference local="region" foreign="code"/>
       </foreign-key>
   </table>
   <table name="departement" description="departement Table">
       <column name="code" type="varchar" size="2" required="true" primaryKey="true" description="code"/>
       <column name="nom" type="varchar" size="50" required="true" description="Name"/>
   </table>
</database>

Il faut ensuite lancer un script qui converti ce fichier en un ensemble de classes que l'on peut alors appeler dans la page web.

Pour phpMyObject

Il n'y a pas besoin de déclarer le schema quelque part. Il le "devine" à chaque fois que l'on veut faire une requête, ce qui le pénalise d'ailleurs en terme de performance. Notez cependant que d'après son auteur, la prochaine version sera plus optimisée : les informations du schema seront apparement stockées dans un fichier cache, évitant cette "divination" à chaque requête.

Notez aussi que cette "divination" n'est pas forcément un avantage décisif pour PMO par rapport aux autres frameworks, parce que d'une part on ne peux pas créer un objet qui serait mappé seulement sur une partie d'une table (ce qui pourtant peut être intéressant dans certains cas pour des raisons de performances, en effet, il faut systématiquement faire un select * apparement), et d'autres part parce que les autres framework proposent un script en ligne de commande permettant de créer les classes ou fichiers XML très rapidement.

Les tests

Les tests consistent en une série de fonctions, qui effectuent un traitement correspondant à une requête SQL précise, et seul le temps d'exécution de ces fonctions est calculée. C'est à dire qu'il n'est pas pris en compte le temps chargement/parsing du script, l'initialisation du framework ORM etc.

Je n'ai fait que des tests effectuant des requêtes de type SELECT. D'une part, parce que les tests sont suffisamment long à faire vu le temps dont je dispose, et d'autre part parce que la majeur partie des requêtes SQL effectuées par un site web classique sont des SELECT. Par exemple, avec un CMS ou même un wiki, le contenu des pages est bien plus consulté qu'il n'est modifié durant toute la vie du site.

À la fin de chaque fonctions de test, l'objectif est d'obtenir un tableau PHP contenant les objets correspondants aux enregistrements sélectionnés.

Pour chaque type de tests, j'ai essayé d'utiliser la meilleure façon de faire pour chaque framework ORM, d'après ce qui est dit dans les manuels en ligne. Maintenant, n'étant pas expert non plus avec ces frameworks (mise à part jDao), peut-être existe-t-il des astuces permettant de faire plus performant avec tel ou tel framework. N'hésitez pas alors à me le signaler.

Les mesures indiquées sont la moyenne de cinq mesures consécutives effectuées d'affilé, et données par le module xdebug. Si dans une série il y avait une valeur exagérément trop haute (ça peut arriver si le test a été interrompu trop longtemps par un autre processus du système), elle était éliminée et le test relancé.

Test 1 : benchLittleSelect

Il s'agit ici de récupérer 20 villes de la table cities. C'est à mon sens une requête simple et représentative de bon nombres de petites requêtes que l'on trouve sur un site, pour remplir une listbox par exemple, ou afficher les messages d'un forum.

Doctrine :

   $cities = $conn->query('FROM city LIMIT 20');

J'ai essayé, d'utiliser une autre manière, pour voir si il y avait plus performant, en faisant :

   $cities = Doctrine_Query::create()->from('city')->limit(20)->execute();

Mais les résultats sont similaires.

Propel :

   $c = new Criteria();
   $c->setLimit(20);
   $list = CityPeer::doSelect($c);

PMO :

   $controler = new PMO_MyController();
   $map = $controler->queryController("SELECT * FROM city LIMIT 20");

jDao : J'appelle simplement la méthode getByLimit définie dans le fichier XML

       $liste = jDao::get('city')->getByLimit(20);
       $results=array();
       foreach($liste as $dep){
           $results[]=$dep;
       }

Pour les sélections, jDao renvoi toujours un itérateur sur les enregistrements fournis par la base de donnée, ce qui évite la construction d'un tableau et économise de la mémoire. Aussi, pour être à égalité avec les autres ORM en terme de résultats, c'est à dire avoir au final un tableau contenant les objets correspondants à chaque enregistrement, j'ai rajouté une boucle remplissant un tableau. Et ce sera la même chose dans les autres tests.

Résultats : Doctrine : 55 818; Propel : 29 209; PMO : 25 841; jDao : 13 690

Test 2 : benchLittleComplexSelect

On récupère 20 villes, mais avec les départements correspondants. Il s'agit donc ici d'un select avec une jointure simple entre la table city et la table departement.

Doctrine :

  $cities = $conn->query('FROM city c, departement d WHERE c.region = d.code LIMIT 20');

Pas de résultat pour ce test. Il y a en effet un bug dans Doctrine, qui génère une requête SQL invalide : il manque une virgule entre les deux tables city et departement dans la clause FROM. En effet, la requête que l'on donne à Doctrine est un pseudo SQL. Doctrine parse ce pseudo SQL et régénère derrière une vraie requête SQL. J'ai essayé de voir si la version en développement de Doctrine corrigeait le bug, mais ce n'était pas le cas à l'époque du test, (je n'ai pas reessayé depuis). J'ai essayé aussi en faisant l'autre méthode :

 $cities = Doctrine_Query::create()->from('city c')
      ->leftJoin("departement d")->where('c.region = d.code')->limit(20)->execute();

Mais la sanction est la même : erreur de syntaxe SQL.

Propel :

   $c = new Criteria();
   $c->setLimit(20);
   $liste = CityPeer::doSelectJoinDepartement($c);

PMO :

   $controler = new PMO_MyController();
   $map = $controler->queryController("SELECT * FROM city,departement WHERE city.region = departement.code LIMIT 20");

jDao :

       $liste = jDao::get('citydep')->getByLimit(20);
       $results=array();
       foreach($liste as $dep){
           $results[]=$dep;
       }

Résultats : Doctrine : na; Propel : 31 149; PMO : 54 287; jDao : 13 597

On peut remarquer que PMO ne semble pas apprécier les jointures, vu la différence énorme avec les autres ORM et par rapport à une requête simple. Propel perd un peu en performance. Seul jDao reste constant.

Test 3 : benchMediumSelect

On va récupérer ici tous les départements de la table departement, ce qui correspond à une centaine d'enregistrements. Là encore, j'ai pris un exemple qui arrive quelque fois dans certains formulaires où il faut remplir une listbox.

Doctrine :

   $deps = $conn->getTable('departement')->findAll();

Propel :

   $list = DepartementPeer::doSelect(new Criteria());

PMO :

   $controler = new PMO_MyController();
   $map = $controler->queryController("SELECT * FROM departement");

jDao :

       $liste = jDao::get('departement')->findAll();
       $results=array();
       foreach($liste as $dep){
           $results[]=$dep;
       }

Résultats : Doctrine : 217 274; Propel : 50 565; PMO : 117 030; jDao : 37 440

Bien qu'il n'y ait que deux champs sur la table departement (contre 5 dans city), les performances s'éffondrent pour Doctrine et PMO...

Test 4 (benchBigSelect) et test 6 (benchHugeSelect)

Il s 'agit du même test que le test 1, mais au lieu de récupérer 20 villes, on en récupère 1000 (test 4) et 10 000 (test 6). Ce genre de sélection n'a pas vraiment de réalité dans un contexte de génération de page web, mais peut en avoir une dans le cadre d'un script type "batch" (ou peut être aussi dans le cadre d'un service web, qui sait). Toutefois, l'objectif principal ici est de mieux observer les différences et d'aller aux limites des ORM.

Résultats test 4 : Doctrine : 2 870 100; Propel : 547 152; PMO : 1 330 650; jDao : 220 285
Résultats test 6 : Doctrine : ?; Propel : 5 155 526; PMO : 11 830 391; jDao : 2 041 419

Pas de résultats pour Doctrine pour le test 6 : après de longues secondes d'attentes, PHP met fin au script à cause du manque de mémoire, ou même de crash. J'ai réalisé un petit graphique qui montre l'évolution des performances en fonctions du nombre d'enregistrements récupérés :

On observe que pour Doctrine et PMO, c'est catastrophique. L'évolution est exponentielle. Je pense qu'il y a un souci au niveau de l'implémentation. À contrario, je trouve que Propel et jDao s'en sortent bien.

Test 5 : benchBigComplexSelect

Par curiosité, j'ai fait le même test que le test 2, avec une jointure entre city et departement, mais en récupérant cette fois-ci 1000 enregistrements.

Résultats : Doctrine : na; Propel : ?; PMO : 1 516 136; jDao : 230 706

Pour Doctrine, comme il s'agit d'une jointure, on a le même bug que dans le test 2, donc je n'ai pas pu mesurer. Propel, curieusement, n'arrive pas jusqu'au bout : mémoire insuffisante.

Test 7 : benchStressLittleSelect

Il s'agit de faire ici une sorte de simulation de charge : on calcule le temps que prend la récupération de 20 villes par 100 "pseudo" clients web. Concrêtement, on récupère 100 fois de suite 20 enregistrements. Cela nous permet donc d'avoir une idée du comportement de chaque ORM sous la charge. Mais je reconnais, c'est seulement une "idée", il faudrait faire des tests plus poussés, avec Tsung par exemple. Et encore, les résultats ici sont "avantageux", dans la mesure où on ne calcule que le temps de la requête, et on n'inclus donc pas le temps de parsing de PHP, d'initialisation du framework ORM etc.. Mais je pense que l'on peut obtenir un classement équivalent dans des conditions réèlles. À vérifier.

Doctrine :

   for($i=0; $i < 100; $i++) {
       $cities = $conn->query('FROM city LIMIT 20');
   }

Propel :

   for($i=0; $i < 100; $i++) {
       $c = new Criteria();
       $c->setLimit(20);
       $list = CityPeer::doSelect($c);
   }

PMO :

   for($i=0; $i < 100; $i++) {
       $controler = new PMO_MyController();
       $map = $controler->queryController("SELECT * FROM city LIMIT 20");
   }

jDao :

     for($i=0; $i < 100; $i++) {
           $liste = jDao::get('city')->getByLimit(20);
           $results=array();
           foreach($liste as $dep){
               $results[]=$dep;
           }
     }

Résultats : Doctrine : 4 297 492; Propel : 1 414 115; PMO : 2 603 541; jDao : 566 579

Test 8 : benchStressLittleComplexSelect

Autre stress : on récupère 100 fois de suite 20 villes avec leurs départements (donc équivalent au test 2).

Résultats : Doctrine : na; Propel : 3 298 295; PMO : 3 249 585; jDao : 558 331

Pour doctrine, toujours la même erreur SQL.

Test 9 : benchStressMediumSelect

On récupère 100 fois de suite tous les départements (donc équivalent au test 3 dans une boucle);

Résultats : Doctrine : 13 620 416; Propel : 3 129 616; PMO : 8 907 413; jDao : 2 066 394

Test 10 : benchStressManyIndividualRecord

L'idée ici est de simuler une forte charge, mais sur la récupération d'un seul enregistrement. On simule donc le genre de requête que l'on fait pour afficher une page d'un article par exemple. Cela permet aussi de tester l'API qui récupére un seul enregistrement. Et on fait cela 1000 fois de suite dans une boucle. Notez aussi qu'il est indispensable de faire ce test répétitif pour mieux voir les différences, car le temps de récupération d'un enregistrement est assez minime et il serait difficile alors de les comparer.

Doctrine :

   for($i=0; $i < 1000; $i++) {
       $dep = $conn->getTable('departement')->find(34);
   }

Propel :

   for($i=0; $i < 1000; $i++) {
       $dep = DepartementPeer::retrieveByPK('34');
   }

PMO :

   for($i=0; $i < 1000; $i++) {
       $dep = PMO_MyObject::factory('departement');
   	$dep->code = '34';
       $dep->load();
   }

jDao :

   for($i=0; $i < 1000; $i++) {
       $dao = jDao::get('departement');
       $dep = $dao->get('34');
   }

Résultats : Doctrine : 15 845 267; Propel : 5 085 127; PMO : 3 322 029; jDao : 1 604 880

Test 11 : benchStressManyIndividualComplexRecord

Idem que le test 10, mais ici on récupère aussi le département associé à la ville.

Doctrine :

   for($i=0; $i < 1000; $i++) {
       $city = $conn->getTable('city')->find(3);
       $dep = $city->departement;
   }

Doctrine fait du "lazy loading". C'est à dire qu'ici on ne peut récupérer le département en même temps que la ville (Doctrine ne fait donc pas un seul select avec une jointure). Et pour charger le département, il faut explicitement l'utiliser, d'où la variable $dep.

Propel :

   for($i=0; $i < 1000; $i++) {
       $c = new Criteria();
       $c->add(CityPeer::ID,3);
       $list = CityPeer::doSelectJoinDepartement($c);
       $city = $list[0];
   }

PMO :

   for($i=0; $i < 1000; $i++) {
       $controler = new PMO_MyController();
       $map = $controler->queryController("SELECT * FROM city,departement WHERE city.region = departement.code AND city.id = 3")
                        ->getMap();
   	$city = $map[0];
   }

jDao :

    for($i=0; $i < 1000; $i++){
           $dao = jDao::get('citydep');
           $dep = $dao->get('3');
    }

Résultats : Doctrine : 19 496 700; Propel : 8 220 690; PMO : 6 622 190; jDao : 1 633 613

Test 12 : benchStressSelectWithCriteria

Il s'agit de tester ici ce que j'appelle le "requêteur dynamique". C'est à dire le moyen de pouvoir faire des requêtes avec des critères non prévu à l'avance, tant en nombre qu'en valeur. Dans Propel on a l'objet Criteria, dans jDao l'objet jDaoConditions et dans Doctrine, il s'agit de l'objet Doctrine_Query. Dans PMO, on n'a rien : il faut construire la requête "à la main".

Doctrine :

   $idCriteria = 200;
   $postcodeCriteria = '%50';
   for($i=0; $i < 1000; $i++) {
       $cities = Doctrine_Query::create()
               ->from('city')
               ->where('id < ? AND postcode LIKE ?', array($idCriteria, $postcodeCriteria))
               ->execute();
   }

Propel :

   $idCriteria = 200;
   $postcodeCriteria = '%50';
   for($i=0; $i < 1000; $i++) {
       $c = new Criteria();
       $c->add(CityPeer::ID,$idCriteria, Criteria::LESS_THAN);
       $c->add(CityPeer::POSTCODE,$postcodeCriteria, Criteria::LIKE);
       $list = CityPeer::doSelect($c);
   }

PMO :

   $idCriteria = 200;
   $postcodeCriteria = '%50';
   for($i=0; $i < 1000; $i++) {
       $sql = "SELECT * FROM city WHERE";
       $sql.= " id < ".intval($idCriteria);
       $sql.= " AND postcode LIKE '" .str_replace("'","''", $postcodeCriteria)."'";
       $controler = new PMO_MyController();
       $map = $controler->queryController($sql)->getMap();
   	$city = $map[0];
   }

jDao :

   for($i=0; $i < 1000; $i++) {
           $dao = jDao::get('city');
           $cond = jDao::CreateConditions();
           $cond->addCondition ('id', '<', 200);
           $cond->addCondition ('postcode', 'LIKE', '%50');
           $cities = $dao->findBy($cond);
   }

Résultats : Doctrine : ?; Propel : 21 089 532; PMO : ?; jDao : 2 043 507

J'y suis peut être aller fort avec 1000 itérations. Mais bon, si Doctrine et PMO n'arrive pas à suivre jDao ;-)... Les scripts ont en effet été interrompu par PHP, car ils prenaient trop de temps. Notez que l'API de PMO ne permet pas de faire ce genre de requête de manière simple : il faut utiliser les bonnes vieilles méthodes "à la main".

Conclusion

En terme de fonctionnalité ou d'API, disons le tout de suite : jDao, et même PMO, sont en retard par rapport à Doctrine et Propel. Par exemple, avec jDao, on ne peut définir de relation entre deux objets DAO. Et avec PMO, bien que l'on puisse récupérer en une seule requête un objet ville qui soit lié à un objet departement, on ne peut récupérer par exemple un objet departement quand on a déjà un objet ville chargé, ce qui n'est vraiment pas pratique.

Cependant, on constatera qu'à aucun moment on ne fait du SQL avec jDao, tout comme avec Propel d'ailleurs. Alors qu'avec Doctrine et pire encore, avec PMO, on n'y échappe pas. Dommage, car pour moi, un ORM doit permettre de faire du vrai mapping : on ne devrait pas faire du SQL, ou au moins, pas en faire pour des requêtes simples comme celles effectuées dans les tests.

À la vue des resultats, je pense que Doctrine n'est vraiment pas à la hauteur en terme de performance (je met de coté ce bug SQL génant qui je pense sera corrigé). Il est franchement à oublier pour le moment. À revoir peut être quand la 1.0 finale sortira, mais je pense que de toute façon, d'un point de vue architectural, il a vraiment un problème et qu'il est peu probable qu'il y ait de grosses améliorations de faite, sauf en cassant l'API de Doctrine. Au passage, je ne comprend pas pourquoi Doctrine remplacera parait-il, Propel, dans le framework Symfony. C'est à mon avis une grosse erreur.

PMO est à mon avis trop jeune et trop peu performant pour être utilisé sur des sites sérieux. D'ailleurs la qualité du code est vraiment moyenne selon moi, et il y a moyen d'améliorer tout ça. Mais, il y a eu quelques améliorations semble-t-il depuis les tests. À surveiller donc du coin de l'oeil.

Étant donné que jDao n'est pas utilisable en dehors de Jelix, le seul qui reste et que l'on peut utiliser en dehors d'une appli jelix, c'est Propel. Je pense qu'il n'a pas de performances catastrophiques, que ça reste un choix raisonnable, d'autant plus que son API n'est pas trop moche. Ce n'est pas pour autant que je l'incluerai dans Jelix car il reste tout de même lourd (c'est tout juste si il n'est pas aussi lourd que jelix en terme de volume de code), et compliqué à installer qui plus est.

jDao obtient les meilleurs performances. Normal quand on sait que les requêtes sont en dur dans le code des classes générées à partir des fichiers XML. Cet enthousiasme est cependant à modérer, car, bien qu'il soit suffisant dans une majorité de cas, il offre moins de possibilités que Propel et son API est un peu vieillote. Toutefois, je me suis déjà lancé dans le développement de son successeur, qui reprendra les mêmes principes sous-jacent de jDao tout en proposant une API plus moderne et du niveau de Propel. Et bien sûr, avec pour objectif principal d'avoir de meilleures performances que Propel (mais qui seront toutefois un peu moins bonnes que jDao).

Si vous voulez lancer vous même les tests que j'ai effectué, téléchargez l'archive contenant toute le code source (1.1Mo) et suivez les instructions du fichier INSTALL. Vous pouvez aussi télécharger le document contenant tous les résultats (format openoffice, 22ko)