Pourquoi jDao ?
Par Laurentj le mardi, octobre 30 2007, 14:15 - Projets - Lien permanent
Il y a de plus en plus de projets PHP visant à proposer une API pour faire du mapping relationnel-objet (ORM). En d'autres termes, offrir la possibilité au développeur de manipuler facilement les enregistrements d'une table, par le biais d'objets. J'ai souvent regardé certains d'entre eux pour voir si il serait intéressant de les incorporer dans mon framework PHP Jelix. Au final, j'ai continué à gardé jDao, l'API de mapping relationnel-objet propre à Jelix (issue de CopixDao du projet Copix), bien qu'au niveau fonctionnalité il soit légèrement en dessous de ce que l'on trouve par ailleurs. Pourquoi ? Principalement pour ses performances.
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 ;-) ).
Commentaires
J'aime bien ce genre de billet. Complètement abscons pour le commun des mortels mais tout à fait intéressant dès lors qu'on à la tête régulièrement enfoncé dans un code PHP avec ce genre de problématiques en tête. Quelque part j'me sens moins seul :-p Pour rebondir quand même sur quelque chose de concret, je ne connais pas Jelix (je le dépiauterai surement un jour pour le plaisir) mais je trouve ton billet intéressant car il met par écrit un exemple concret du genre de réflexion que devrait avoir tout développeur (web plus particulièrement). Malheureusement, on ne sent que trop peu souvent ce type de questionnement (les perfs, le rapport entre le nombre de lignes de codes, l'action effectuée et sa récurrence) dans nombre de projets.
Avoir à disposition un framework qui peut faire le café mais qui nécessite toute la cafetière là ou je n'ai besoin que d'un filtre c'est comme d'employer un bazooka pour tuer une mouche : c'est bourrin ...
Et comment ça se compare à Zend_Db ? Finalement Zend_DB me paraît encore plus simple, non ?
C'est intéressant en effet. L'idéal étant de pouvoir choisir l'ORM que l'on veut utiliser dans son framework.
Il y a quand même un truc ultime avec symfony et propel, ce sont les behaviors http://redotheweb.com/2007/09/02/understanding-behaviors/
@yermat : Zend_Db est plus une couche d'abstraction d'accés aux bases de données (l'équivalent de Pear::Db, PDO ou jDb dans Jelix), qu'un ORM. Et tout les ORM que j'ai cité ont leur propre couche d'accés (Creole pour Propel, un truc spécifique pour Doctrine et phpMyObject, et jDb pour jDao).
@Olivier : je ne pense pas que pouvoir choisir son ORM est idéal, parce que ça veut dire que l'ORM n'utilise pas les spécificité du framework, et donc qu'il y a de forte de chance qu'il y ait doublon de fonctionnalité, donc de code. (et donc perte en perf à cause du surpoid de code à parser par PHP, voir à executer). Un exemple : si j'utilisais Propel ou Doctrine, il y aurait un doublon avec jDb, la couche d'accés aux BD. Et je serais obligé de faire une surcouche pour qu'on puisse utiliser les selecteurs etc..
Au sujet des behaviors, il est prévu un truc similaire dans jDao, mais ça utilisera le système évènementiel de Jelix (jEvent).
Je pense que tu aurais du être plus factuel. Il serait intéressant de faire de vrais benchs JDAO & PMO sur un accès au base mysql sakila de référence pour comparer.
S'il y a vraiment un bloat sur ce point, ça sera facilement hackable. Je vais regarder si je peux faire des benchs ce week end , je te tiens au courant.
Sauf que dans les faits, Doctrine est plus rapide que Propel (principalement car Doctrine s'appuie sur PSO, ce que ne fait pas encore Propel).
Par ailleurs, tu expliques que "je ne pense pas que pouvoir choisir son ORM est idéal, parce que ça veut dire que l'ORM n'utilise pas les spécificité du framework, et donc qu'il y a de forte de chance qu'il y ait doublon de fonctionnalité, donc de code". Dans ce cas, qu'est-ce qu'un framework ? Est-ce un ensemble monolithique de code conçu de A à Z pour fonctionner ensemble ? Ou est-ce bien la "glue" entre différentes librairies, ce qui permet de proposer un haut niveau de fonctionnalités abstraites sans pour autant devoir tout réécrire à chaque nouveau framework ?
Enfin, je vois un inconvénient majeur à jDao : l'impossibilité d'abstraire les requêtes comme Propel peut le faire avec le système de Criteria et Doctrine avec le chaînage des clauses d'une requête.
Alors oui, il y a sans doute perte d'efficacité en terme de temps machine par rapport à des requêtes SQL. Mais c'est pareil dans tous les domaines du développement. Pourquoi s'emmerder avec des librairies javascript quand on peut faire du bon vieux js à la main ? Pourquoi avec des interfaces graphiques quand on a la ligne de commande à proximité ?
@code34 : il n'y a pas besoin de faire de bench pour se rendre compte de la différence. Pour faire une requête, dans phpmyobject ça passe par je ne sais combien de méthodes et de traitements différents, alors que dans jDao, ça tient en quelques lignes de codes qui appellent directement l'API php de la base de donnée en question.
@xavier : possible que Doctrine soit plus rapide que Propel, mais ça reste à mon avis un bon gros bloatware par rapport à jDao (je pense que ma démonstration est suffisement claire là dessus).
Non, un framework, ce n'est pas forcément une glue entre différentes bibliothèques. Par ce qu'à l'origine, ces bibliothèques n'ont pas forcément étaient faites pour travailler ensemble, et donc ne profite pas forcément des possibilités des unes et des autres. Aussi dans jelix ai-je choisi de ne pas réutiliser certaines choses existantes, pour avoir une meilleur intégration des composants dans le framework. Maintenant, dans jelix, j'utilise aussi des classes utilitaires venant de l'extérieur. En fin de compte, tout est une question de compromis.
L'inconvénient que tu sembles supposer exister, n'existe pas dans jDao. jDao propose bien entendu de faire des requêtes avec des critères non défini à l'avance, donc comme Criteria...
Ta comparaison n'est pas valable pour moi. Ton exemple de lib js est plutôt à comparer entre quelqu'un qui fait toute son appli PHP à la main, et celui qui utilise un framework. Et en ce qui concerne les interfaces graphiques vs ligne de commande : ce sont des environnements plutôt complémentaires.
Comme je suis curieux de nature, ta démonstration ne me suffit pas. D'intuition, je pense qu'interroger le cache du SGBD est beaucoup plus rapide que de passer par un parser de XML qui intrinsèquement est lent.
Comme le dit justement aussi Xavier, on peut pas comparer des ORM qui ne proposent pas un niveau d'abstraction équivalent ;) Le rôle d'un framework s'est aussi d'apporter un nombre de fonctionnalités importantes pour un confort de développement.
@code34 : si tu relis bien mes explications, le parsing du fichier XML de jDao n'est fait qu'à la première execution de l'application. C'est à dire que durant toute la vie de l'application, ce parsing n'est plus effectué, les classes PHP correspondantes au contenu du fichier XML étant générée et stockée dans un fichier de cache. L'utilisation de jDao revient donc en fait à charger une classe métier PHP classique que tu aurais codé à la main, contenant de simple requêtes classiques.
Je ne vois pas en quoi jDao est moins ou plus abstrait que phpMyObject ou propel ou doctrine. Il propose des fonctionnalités équivalentes. Comme je l'ai dit à xavier, on peut interroger la base avec une liste de critères dynamiques (c'est à dire, sur une liste de champs non figées). Cela correspond exactement à ce que propose Propel avec Criteria.
Tu concentres toute ton attention un unique problème. Certes la performance d'un Orm est un critère important, et pourtant il y en a d'autres. Notamment toutes les méthodes qui te permettent d'accéder aux données.
J'ai regardé ton code, et j'en ai conclu que Jdao est conçu avec une logique impérative. Tu n'exploites pas à fond l'oo.
Ne t'offusque pas, c'est juste un conseil(que l'on m'a donné il y a quelques mois et qui m'a permis de progresser), il faut s'astreindre à ne pas mettre plus de 10 lignes dans les méthodes (et déjà c bcp).
Généralement sauf cas spécifique (core du programme), tu peux te dire que si tu as plus de 10 lignes, tu as un gros sac de noeud, mal conçu, qu'il faut casser en petit bout. Ce qui te permet ensuite d'optimiser (c'est ce que tu cherches;) .
En continuant comme ça, tu vas avoir un problème en terme d'évolutions et de maintenance du code surtout dans le cadre d'un framework.
Au sujet de ta question sur les fonctionnalités, je ne crois pas que tu gères : - les multiples primary key, - les relations entre objets, - que tu puisses séparer des objets qui proviennent de plusieurs tables à partir d'une seule requête sql, - que tu puisses faire des recherches sur les objets que t'as récupéré dans ta requête, - que tu puisses les lier ensemble etc ...
Un exemple tout bête: pourquoi avoir différencier insert/update dans ta factory. Globalement, ce sont des questions qui ont déjà trouvé des solutions dans PMO. Tu n'as pas la vision de la chose car il y a une couche d'abstraction supplémentaire pour que cela soit plus simple pour le dev.
Dans PMO derrière un: $monobject->commit();
tu as l'équivalent d'un $factory->insert($monobject); ou $factory->update($monobject);
Ce qui revient au même que ce que tu proposes, sauf que le dev n'a pas à se poser la question si c'est un insert ou un update.
Autant je pense que le choix que tu décris au niveau du cache est intéressant, autant je pense que ça n'est pas le critère qui à lui seul justifie le choix d'un ORM.
Je suis à ta dispo pour discuter dev si ça t'intérresse sur msn ;)
Tu oublis une chose : que tu fais du PHP. Il faut donc faire un compromis entre le purisme objet et ne rien faire en objet. En l'occurence, enchainer 15 methodes inutilement pour le plaisir d'avoir des méthodes de moins de 10 lignes, ce n'est franchement pas efficace en PHP. As tu déjà étudier le fonctionnement d'un compilateur / d'un interpréteur ? Moi si, et parmis eux, le moteur PHP.
C'est comme dans PMO où tu fais systematiquement des interfaces : pour la plupart, elles ne servent strictement à rien, et rende ton script moins efficace (temps de parsing plus long, plus de vérifications internes dans le moteur PHP etc..). En PHP, les interfaces sont à reserver pour les objets extérieurs à la classe (par exemple, dans jelix, il y a une interface pour tout ce qui est driver, plugins et autres). Pour les classes internes, les interfaces sont rarement utile (en PHP). La notion de contrat en interne n'est bien souvent pas indispensable, et c'est le cas dans PMO. Par contre l'utilisation des interfaces est utile et n'implique pas de problème de performances dans d'autres langages, en particulier ceux qui sont compilés ou bytecodé (java). En PHP c'est moins le cas.
En ce qui concerne mes méthodes de plus de 10 lignes, ce ne sont pas des sacs de noeuds (pas confondre code complexe et sacs de noeuds), je sais quand il faut découper et quand ce n'est pas nécessaire de découper (ça ne veut peut être rien dire, mais ça fait 15 ans que je fais de l'objet). Faire une méthode n'est utile que pour, soit proposer un point d'entrée externe (API), soit permettre de surcharger une partie du code par héritage, soit factoriser le code. Mais faire une méthode supplémentaire parce qu'une autre fait plus de 10 lignes, ça n'a pas de sens. Et je te retourne le conseil : va revoir ton code, parce qu'en terme de sac de noeud, c'est pas mieux. Et je dirais même que c'est pire : en suivant le fonctionnement de ton code, et parce que tu fais plein de petites méthodes dans tout les sens, on saute de méthode en méthode dans tout les sens. Dur à suivre. Donc dur à maintenir. Ça me rappelle les programmes en COBOL avec des GOTO de partout, que je devais faire évoluer à l'UAP.
Jelix est un fork de Copix. J'ai bossé sur Copix depuis 2001, en l'utilisant dans de nombreux projets pro, et les rares problèmes d'évolutions n'ont jamais été dû à "des sacs de noeuds" ou des méthodes de plus de 10 lignes.
si bien sûr, jDao supporte ça...
C'est prévu. Mais depuis plusieurs années que jDao/CopixDao existe, ça n'a jamais vraiment été un problème.
Peut être que tu trouves cela très important, et je sais que c'est le point que tu met en avant pour PMO. Personnellement, je ne trouve pas ça vraiment utile.
Je vois pas de quoi tu parles. À priori, si tu dois faire des recherches, c'est par ta requête (le SGBD le fera beaucoup plus efficacement que le meilleur code PHP).
Ça rejoint le point sur les relations entre objets.
pattern DAO. Mais ça pourrait être amélioré effectivement.
Je n'ai pas dit que c'est le seul critère. Ce que j'ai dit, c'est que les quelques fonctionnalités que proposent les autres ORM en plus de jDao ne valent pas le gain de performance que l'on a avec jDao. Et puis, peut-être ne le sais-tu pas, mais overblog.com, une plateforme de blog qui a des millions de pages lues par jour, a choisi Jelix (et ils utilisent jDao) et ce n'est pas par hasard (en clair, ils ont jugés eux aussi que Propel ou Doctrine étaient trop bloatware vis à vis leurs besoins).
Désolé, pas de msn chez moi... J'utilise plutôt des protocoles ouverts (IRC par ex).
Il y a des choses que tu ne prends pas en compte, en conséquence tu ne vois pas l'utilité d'utiliser du code objet avec une logique plus OO. C'est aussi ça le travers de PHP d'avoir des reliquats de style de programmation, de pouvoir mélanger de l'impératif avec de l'oo.
Cette rengaine s'habille souvent d'un discours sur la performance mais en fin de compte, le gain n'est pas si important que ça par rapport à ce que tu perds.
Plusieurs sites payent aujourd'hui (comme linuxfr) ce manque de clairvoyance. Ca ne veut pas dire que c'est mal fait, au contraire, ça veut juste de dire que l'optique s'était de faire de la perf, d'optimiser pour que ça marche sur une seule et unique petite machine, aujourd'hui ils le payent.
D'ailleurs, je crois qu'il est même question de ne plus utiliser du php, mais d'aller vers un langage plus oo.
Si tu compares ton critère - performance
et les autres critères: - facilité à lire le code - facilité à maintenir, faire évoluer le code - travail en équipe etc ..
Au final tu n'es pas gagnant. C'est plus simple de rajouter ou faire évoluer les composants d'une machine que de maintenir du code optimisé/illisible pour la plus part des développeurs.
Il faut concevoir des architectures logicielle structurées avec des objets qui échangent des messages. Les appels de méthodes on pu te frapper dans PMO, mais ce ne sont ni plus ni moins des objets qui communiquent.
D'ailleurs ton explication sur l'interpréteur ne m'a pas convaincu. Mais bon je ne mets pas en doute ta parole, ni le fait que l'interpréteur php ne soit pas optimisé pour le code objet, ça semble d'ailleurs évident, il demeure de tels abbérations (..)
C'est dommage que tu ne sois pas plus à l'écoute car s'est intéressant de savoir pourquoi en OO, on utilise des interfaces, ou on privilégie les appels de méthodes entre objets plutôt que des portions de code complexe. Ca n'a rien à voir avec le langage.
Les interfaces comme l'UML font parti de la conception.
@code34 : tu es en train de me réciter ton cours là ? On voit que tu es encore étudiant, et que tu as bien appris tes leçons. Tu verras qu'avec l'expérience, tu auras un discours plus pragmatique. J'ai bossé pendant plusieurs années sur des projets professionnels, en équipe, avec un framework PHP similaire à celui de Jelix (Copix pour ne pas le nommer). Nous n'avons jamais eu de problèmes à travailler en équipe, à maintenir le code, à le faire évoluer, et pourtant il contenait des méthodes de plus de 10 lignes. Faire des méthodes et des objets de partout n'est franchement pas un critère de lisibilité et de maintenabilité (mais, et je le répète au cas où : je n'ai rien contre l'objet et je fais de l'objet). Car à pousser à l'extrême comme tu le fait dans PMO, on en arrive à faire du code spaghetti.
Jelix et jDao sont 100% objets. Ce n'est pas parce qu'on fait des méthodes de plus de 10 lignes qu'on ne fait plus de l'objet. On peut se permettre de pratiquer sa religion puriste en objet avec certains langages. Mais pas avec PHP, de par son architecture et de ses lacunes.
Mais je sais très bien tout ça. Je n'ai pas fait que du PHP dans ma vie. J'ai même utilisé des bases de données objets, c'est dire.
Si justement. Il faut savoir adapter sa façon de travailler avec le langage utilisé, et pas essayer de faire des choses qui manifestement ne sont pas ce qu'il y a de mieux à faire avec ledit langage.
Tu veux faire du pure objet ? Ne choisi pas PHP. Fais du Python, ou du Eiffel par exemple.
Maintenant, la programmation purement objet n'a jamais été une solution à tout. La preuve, les langages fonctionnels ont un regain d'intérêt ces dernières années.
Non, je ne suis pas étudiant, et oui j'applique ce que j'ai appris en cours.
Tu n'as toujours pas répondu à ma question, combien de temps cpu fait gagner ton système de cache pour chaque execution de script par rapport à ce que font les autres ORM.
@code34 :
Pourtant....
Minute papillon, j'ai une vie en dehors de l'informatique :-p Les benchs arrivent. Pour faire rapide, PMO est 2 à 10 fois plus lent que jDao selon le type de select et le volume de donnée à récupérer. Les temps de réponse semblant augmenter de façon exponentiel au volume de données (donc sur un site à haute fréquentation...). J'ai commencé aussi pour doctrine (d'aprés les premiers tests, c'est pire, un facteur 3 au minimum), et je ferais Propel ensuite.
Bilan à la fin du week-end max.
Je prends des cours du soir en // de mon travail.
Ce bench va être instructif, cependant je pense que le point que tu as levé concernant les perfs lié au cache de tes classes n'est pas significatif en tout cas pas autant sur PMO que sur Jdao.
J'ai implémenté le même type de cache de classe aujourd'hui dans PMO.
Il y a un gain de perfs à peine perceptible par rapport au temps d'exec global alors que les schémas sont écrits en dur dans les classes de description de table.
J'ai comparé les résultats de temps d'exec entre la version 0.07 et celle qui est actuellement en dev 0.11 et c'est du kifkif.
Après pour la consommation expo en fonction du volume de donnée, je suis impatient de voir ce que tu as fais :D
Conclusion : soit l'implémentation de ton cache est foireuse. Soit tes calculs de perfs sont foireux. Si tu as maintenant un système de cache et que ça n'apporte rien, c'est que tu as vraiment un problème quelque part.
Et ce n'est pas parce qu'un cache n'apporte rien chez toi, que ce n'est pas significatif dans d'autres implémentations ORM.
Le gain qu'apporte ce genre de techno est non significatif par rapport à d'autres traitements. Tout dépénd de l'abstraction qu'apporte ton Orm.
Avec Jdao t'as mappé les commandes mysql, le traitement est simple, t'as un accès quasi direct à ton sgbd, donc le temps d'éxecution est quasi nul. Si tu n'utilises pas ton cache, ton temps d'exec peut doubler. Donc oui ton cache sert ;)
Avec PMO, quand tu passes une requête SQL, t'as la moitié du temps qui est consommé à contrôler l'unicité des records. Si t'en as 10 c'est vite fait, si t'en as 6000 ça prend plus de temps. Mais qui serait assez fou pour faire des select * from xxx sur des grosses tables sans clause where ou limit ???
Deux records différents dans la liste ne doivent pas représenter la même donnée en base. Pour cela, les objets sont signés. Et pour chaque objet récupérer, PMO contrôle si la signature existe ou non. Pour cela, il utilise une map hashée signature sha1 ->référence record pour garder un temps d'exec linéaire. (ce système est très certainement améliorable).
Cette base de cohérence sert notamment dans la gestion des relations entre record. Si tu modifies un record, tu sais que celui-ci reste liés aux autres record.
PMO n'a pas été conçu dans l'optique de faire de la perf, mais il n'a pas été conçu non plus dans l'optique de faire de la non perf. J'imagine que pour Doctrine c'est le cas aussi.
Le but est de faire tout les traitements qui concernent la couche modèle au niveau de la couche modèle, et de ne pas remonter ces traitements au niveau du contrôleur.
Vous avez l'air de concevoir vos classes pendant que vous écrivez le code. Je pense qu'il ne faut pas oublier qu'une classe représente un concept et a un certain nombre de responsabilités. Le choix des méthodes se fait bien en amont de l'écriture du code. En tout cas chaque méthode doit remplir une fonction simple et qui a du sens par rapport à la classe. Je ne pense pas que de créer une méthode, que ce soit pour réduire la longueur d'une autre méthode ou pour factoriser du code soit une très bonne façon d'écrire de l'orienté objet.
Enfin, je trouve incroyable de coder en orienté objet dans un langage où l'appel de méthode ou la définition d'interface peut être vu comme une perte de performance. Il y a un problème dans le choix de l'outil à mon avis là :)
Perso, je travaille avec des diagrammes de classe. Et ça me semble logique et incontournable d'utiliser les interfaces.
Par rapport à ce qu'a dit Laurent concernant le nombre de lignes :
Je pense qu'il a fait un raccourci intellectuel . Le but n'est pas de créer de bêtes méthodes à la volée pour faire moins de ligne.
Quand il y a une méthode avec un grand nombre de lignes, cela sous entend que le problème a été mal analysé en amont, et qu'il y a un problème de conception. Justement ça peut être une classe qui a trop de responsabilité. Il faut donc repasser par la case UML et décortiquer le problème.
@code34
C'est pour ça qu'il y a un cache de code. Ce n'est pas parce que les autres n'en ont pas qu'utiliser un cache est mal.
C'est bien ça le problème. À quoi bon faire ce traitement à chaque fois que l'on veut faire une requête, alors que pendant la plupart du temps de la vie d'une appli, le schema de la table ne change pas ?
Oui, c'est fou de faire ce genre de grosses requêtes. Mais tu oublie juste un petit détail qui a son importance : il n'y a pas qu'un internaute à la fois qui visite ton site web. Il paraitrait même qu'il y a des sites qui ont plusieurs centaines de hit à la minute, voire des milliers, et donc autant de simples requêtes executées en même temps. Donc le temps que prend ton processus pour récupérer seulement 10 enregistrements, qui semble négligeable sur ta machine de dev, peut avoir des conséquences énorme quand le processus est exécuté 100 fois en même temps, et même, est plus consommateur qu'une seule requête qui récupérerait 1000 enregistrements (facilement démontrable, tu le verras dans les benchs).
J'ai bien compris tout ça.
Pourtant la performance est un élément important pour une application professionnelle, d'autant plus quand le site devra être soumis à une forte charge.
@benoit
Ça m'arrive oui. Bien que je conçois mes objets et API bien avant de coder, il y a des choses que je fais directement, parce que simples. D'une part parce qu'après 20 ans de programmation, on acquiert des reflexes et on sait de suite comment faire telle ou telle chose simple. Mais aussi parce qu'il y a aussi des méthodes de développement qui ne nécessite pas que tout soit écrit noir sur blanc en bon français dans un document de 50 pages et dans des dizaines de schemas UML. Peut être en avez vous déjà entendu parler : il s'agit des méthodes agiles en développement (extreme programming etc.), qui sont, à mon humble avis, plus adaptées à la réalité industrielle d'aujourd'hui (c'est d'ailleurs pour ça qu'elles connaissent un succés grandissant) que les autres que l'on trouve encore bien souvent dans les SSII, lourdingues tant pour les développeurs que pour le client (mais géniale financièrement, car permettant de facturer énormément de temps au client, sans toutefois une garantie de succés au finale).
Pas forcément. Les détails de l'implémentation n'ont pas forcément à se faire lors de la phase conception (c'est ce que prône les méthodes agiles pour certaines). Tout dépend de la complexité des rêgles de gestion, de l'application etc.. On fait la phase de conception avant de coder, pour définir l'ensemble des objets et des API qu'il faut développer, sans forcément rentrer dans les détails techniques donc dans la définition des méthodes "bas niveau" techniques. Ce qui n'est pas forcément une mauvaise chose, puisqu'on ne perd pas de temps à repasser dans la phase conception quand il faut changer des détails d'implémentation pour x raisons (qui n'ont pas forcément à voir avec les règles de gestion de l'application).
Ai-je dit le contraire ?
C'est ce que j'ai dit à code34 justement...
Ah ? Vous n'avez jamais maintenu ou fait évoluer des applications vous ? Quand on doit faire évoluer une application, que l'on passe par la case UML ou directement à la case de l'écriture du code, il arrive bien souvent que ces évolutions contraignent à faire de nouvelles méthodes (protégées ou pas, peu importe), afin de mutualiser des bouts de codes (des détails de l'implémentation). On fait de la factorisation quoi, et c'est ce dont je parlais.
Oui et non. C'est le problème de bon nombre de langage de script pour le web : à partir du moment où, à chaque fois que l'on a une requête HTTP, il faille analyser le script avant de pouvoir l'exécuter (comme c'est le cas pour perl, PHP, ASP et autre), on n'a pas d'autres choix que de limiter son enthousiasme de puriste en POO. Maintenant il existe des caches d'opcode pour PHP (donc les scripts sont compilés et mis en cache), mais ils sont rarement présent chez les hébergeurs car ça prend énormément de mémoire (ce serait sucidaire de l'activer sur un hébergement mutualisé).
Maintenant, il ne faut pas généraliser non plus ce que dit code34 à propos de mon implémentation. Les méthodes de jDao ne font pas des centaines de lignes de code et sont structurées comme il se doit. Sans compter que la génération de code est un processus relativement complexe, surtout dans le cadre de jDao où on doit prendre en compte beaucoup de paramètres, et qu'il n'est pas forcément évident de scinder tout ça en 15 000 méthodes de 5 lignes.
Merci donc de ne pas critiquer ce que je pourrais avoir fait sans avoir vu le code en question.
@code34
Franchement, c'est vraiment n'importe quoi ce que tu dis là. C'est totalement infondé. C'est même trollesque. Trouve moi des démonstrations sur la corrélation "méthode avec un grand nombre de lignes = problème". Tu ne pourras en aucun cas le prouver de façon générale, puisque ça dépendra de ce que fait cette méthode.
Mais maintenant, si tu trouves vraiment que mon code est pourri parce que j'ai une méthode qui fait 279 lignes, vas-y te gène pas, propose un patch.
Et pendant que tu y es, va voir ce que fait cette classe de Gecko (que je n'ai pas écrite) : elle contient beaucoup de méthodes de plus de 100 lignes. Et va dire aux développeurs de Mozilla qu'ils ont un problème avec cette classe (Encore faut il que tu comprennes ce que ça fait...).
Les méthodes agiles n'ont jamais préconisé de ne pas écrire de spécification pour les classes que l'on va coder. C'est un raccourci que de nombreux développeurs prennent car ils veulent coder tout de suite mais je doute fortement que ça fasse gagner du temps et donc de l'argent au client, bien au contraire.
Entre l'écriture d'une spécification monolithique et fixe en début de projet et un refactoring de code constant il y a un intermédiaire qui est l'écriture de spécification qui évolue. Je pense que les méthodes agiles sont plus proches de cet intermédiaire.
PS : Je ne critique en rien ton code, j'exprime mon désaccord sur les affirmations que tu portes dans les commentaires de ton blog.
Au fait précision : les "énooooorrrmmes" méthodes de jDao dont parle code34, sont des méthodes qui ne sont appelées qu'au premier lancement de l'appli, et non pas à chaque page ou requête. Enfin bref, la grosseur de ces 2-3 méthodes n'a pas pour origine d'améliorer les perfs de ces méthodes. C'est juste qu'elles sont complexes et pas évidentes à scinder, et que les scinder n'apporterait pas grand chose selon moi, si ce n'est encore plus de complexité, d'autant plus qu'elles n'ont quasiment aucune interaction avec des objets externes.
Cela n'est pas lié au schéma de la base.
Le contrôle d'unicité des objets se fait sur les données renvoyés par le sgbd. Ca a une utilité quand tu utilises plusieurs tables dans ton select.
Je n'ai pas dis ça. Toutes les remarques sont bonnes à étudier.
Bien sûr que si !En fait, non, PMO n'est pas lié au schema, mais il doit à chaque requete (SQL ou en tout cas, HTTP), deviner la structure des tables pour faire le dispatch dans tes objets. Conclusion, comme je le dis dans mon billet, cette répétition de processus de divination est absurde, puisque le schéma de la base de donnée ne change quasiement jamais durant la vie de l'appli. C'est du gachis de ressource, et pénalise l'application elle même.Enfin bon, désolé, j'arrête cette discussion là avec toi, je trouve qu'elle n'apporte plus rien.
@code34 : Juste une dernière chose. Doctrine (et peut être Propel, je n'ai pas encore regarder en détails son API), fait lui aussi des requêtes avec jointure, et un dispatch des valeurs sélectionnées dans les objets correspondants aux tables. Comme PMO quoi... (Sauf qu'il ne devine pas la structure, c'est au développeur de lui indiquer...)
Le mécanisme de cache est intégré dans la prochaine version donc le problème ne se pose plus. Cache qui est généré automatiquement la première fois via le mécanisme de découverte.