30 juil. 2010

Les ORM sont-ils une bonne chose ?

Axiom Database LogoCes dernières années, avec l'explosion du web dynamique et les avancées liées à la programmation objet, on a vu se développer des systèmes permettant de faire des liens plus ou moins directs entre le monde objet et le monde relationnel. Ces ORM (pour Object/Relational Mapping) proposent de créer une couche entre votre application et les données sous-jacente. En somme, ils gèrent ce qu'on appelle la persistance des données, en tentant de répondre à une question toute simple : comment faire pour les utiliser simplement tout en gardant une architecture claire, concise et efficace ?

Bien évidemment, ils échouent à apporter la bonne réponse à cette question.

Introduction

Vous avez peut-être entendu parler de MDA, l'acronyme pour Model-Driver Architecture - ou architecture dirigée par le modèle. MDA est une émanation de l'OMG à qui l'on doit aussi UML et d'autres standards tout aussi importants. Pour faire simple, la grande idée de MDA est de permettre la génération de code en fonction de la définition d'une architecture haut niveau. Des outils transforme les diagrammes définis par l'architecte en code fonctionnel.

Si l'idée est plus ou moins sympathique, il reste difficile de la mettre en pratique - sauf dans des cas particuliers. Il est assez évident qu'une architecture haut niveau ne contient pas assez d'information pour permettre la génération complète d'un produit ; et si c'est le cas, alors cette architecture est allée tellement loin dans les détails qu'il a fallut autant de temps pour la réaliser qu'il en aurait fallu pour réaliser le produit dans une autre technologie[1].

Le mapping objet/relationnel est l'un de ces cas particuliers dont je viens de parler. Une fois défini le modèle de la base de donnée, on peut générer un gros morceau de code qui va permettre de faire le lien entre cette base et le (ou les) programme(s) qui vont l'utiliser. Le gain de temps est plus qu'appréciable dans ce cas puisque la définition du modèle de donnée est aisée et rapide. Tout semble aller bien dans le meilleur des mondes, au point que cette approche est encensée un peu partout et que des dizaines de frameworks ont été développés en Java, en C#, en C++, en PHP, en python, etc. Une simple recherche sur "ORM" dans Google renvoie plus de 19 millions de réponses[2]. Le monde entier aime les frameworks ORM.

Alors qu'est-ce que je leur reproche ?

Point 1 : stable, mais concret et volatile

Puisqu'il représente une partie non négligeable du métier d'un produit, le code généré par un ORM est fait pour être utilisé par une grosse partie de ce produit. En architecture objet, c'est ce qu'on appelle la stabilité[3]. Plus un module est utilisé par d'autres modules, plus il est stable. Il faut comprendre que la stabilité est une conséquence de l'architecture : elle n'existe pas à priori.

Un module stable est peu susceptible d'évolution, car toute évolution peut avoir des conséquences désastreuse pour le projet puisque beaucoup d'autres modules dépendent de celui-ci. Par conséquent, plus un module est stable, plus ils doit respecter le principe ouvert/fermé. Pour qu'un tel module soit extensible, il faut qu'il présente des abstractions fortes et les mécanismes qui permettent de redéfinir ces abstractions. Au vu des technologies actuelles, ça veut dire que le module doit présenter suffisamment de classes abstraites.

Ces deux notions - stabilités et abstraction - peuvent être définies à l'aide de métriques. I, qui varie de 0 à 1, est l'instabilité (c'est à dire le contraire de la stabilité) et est définie à partir du moment ou le module existe dans un ecosystème d'autres modules et qu'au moins un module lui est lié par une relation de dépendance. Pour calculer I, on prends en compte les dépendances afférentes (des modules externes dépendent de ce module) et les dépendances efférentes (le module dépends d'autres modules).

Da est le nombre de dépendances afférentes.
De est le nombre de dépendances efférantes.

I = De / (Da + De)

Le calcul de l'abstraction A (variant aussi de 0 à 1) est plus simple : si C est le nombre de classes publiques du module, et Ca le nombre de classes abstraites publiques, alors

A = Ca / C

Un module stable doit être abstrait. Un module instable (donc un module dont peu de monde dépends) est généralement un module situé très haut dans les couches de l'application. Il défini les fonctionnalités de celle-ci. Il est par conséquent concret, puisqu'il n'a pas besoin d'être étendu. On voit se dessiner une relation idéale entre I et A. Celle-ci est simple :

I = 1 - A

Vu que c'est une relation idéale, elle a peu d'intérêt en soit. Elle dit juste que idéalement, plus le module est instable, moins il est abstrait. Ce qui nous intéresse le plus souvent est de savoir à quel point on s'éloigne de cet idéal. Pour ce faire, on calcule simplement distance D entre la ligne idéale et les valeur d'instabilité et d'abstraction réelles. Une formule simple nous donne une solution tout à fait acceptable :

D = abs(I + A - 1)

Dans l'idéal, on doit minimiser D (qui varie de 0 à 1) au mieux. Plus D est proche de 0, plus on s'approche de l'idéal donné par la relation ci-dessus. Une valeur proche de 1 signifie qu'on a soit un module stable et concret (problématique en termes de maintenance), soit un module instable et abstrait (donc d'aucune utilité).

Revenons maintenant à notre code généré. Est-il stable ? Oui. Le nombre de modules qui va dépendre de notre code généré risque d'être très élevé, d'autant plus que le nombre de module dont notre code va dépendre est lui très réduit. Donc I tends vers 0. Est-il abstrait ? Le principe de génération utilisé par les ORM est de dire qu'on associe une table (ou une requête) à une classe. Cette classe n'est pas abstraite - ce qui est logique, puisqu'elle représente quelque chose de véritablement concret. Par conséquent, l'interface de notre module est purement concrète, et A = 0. La conséquence est immédiate : si I et A sont proches de 0, alors D est proche de 1.

Il existe des cas ou une telle propriété est bienvenue : par exemple, si le module considéré est une surcouche à une API, alors cette surcouche sera par définition très concrète et stable. Pourquoi ces cas-là sont-ils acceptable ? Et bien parce que l'API étant peu volatile, le module est peu sujet à modification. D'ou la règle : un module stable et concret est un module qui n'est pas sujet à modification.

Une base de donnée est susceptible d'être amendée avec de nouvelles tables, de nouvelles colonnes dans une table, ou de nouvelles relations entre plusieurs tables. Puisque la base de donnée vit, elle est très volatile. Et puisque le code généré par l'ORM est d'une part basé sur le modèle de donnée et d'autre part basé sur les spécifications du produit, il résulte que le code généré par un ORM est très volatile - il a de fortes chances d'être modifié au cours de la vie du produit. La conséquence est directe : une modification, même peu importante, risque d'avoir un impact important sur le projet, avec tous les problèmes de maintenance et de tests que cela implique.

Point 2 : une différence de paradigme

Les ORM, en créant une relation entre une architecture objet et un modèle de données relationnel, tentent de résoudre la quadrature du cercle. Ils définissent des classes pour représenter le résultat de requêtes. Cette approche architecturale a un nom : c'est une approche dite ontologique pure, puisqu'elle définit l'architecture en se basant uniquement sur les propriétés connues des entités qu'elle représente.

Une approche ontologique produit des entités concrètes - elle n'est pas capable de déceler des abstractions. Prenons un exemple simple : un jeu type asteroid. Le joueur pilote un vaisseau, et tire des missiles sur des astéroides en mouvement. Une approche ontologique me permet de définir une classe Vaisseau avec les propriétés (position, vitesse, accélération,,nombre de rampes de lancement de missiles) et des méthodes représentant les différentes actions qu'on peut effectuer avec ce vaisseau, une classe Missile avec une autre liste de propriété et une classe Asteroide avec là aussi une liste de propriétés propres. L'approche ontologique pure me dit que les relations entre les différents éléments sont directe - c'est à dire qu'il existe une dépendance directe entre Vaisseau et Missile, entre Missile est Asteroide et entre Vaisseau et Asteroide. Tout va bien dans le meilleur des mondes, jusqu'à ce que je rajoute une autre entité - par exemple, des ennemis représentés par une classe Ennemi. Je suis alors obligé de rajouter le traitement des collisions entre un astéroïde et l'ennemi, entre le vaisseau et l'ennemi, et entre le missile et l'ennemi. Puis je rajoute un Planetoide fixe avec une gravité - et je crée de nouveau des liens, dont le nombre augmente en suivant une fonction factorielle du nombre de classes déjà présentes. Au bout de très peu de temps, mon projet devient horriblement difficile à maintenir car tout utilise tout, une modification a un endroit provoque un problème autre part, etc. Ma conception pourrit, littéralement.

Les architectures établies avec une approche ontologique pure ont toute le même problème : elles pourrissent d'autant plus vite qu'elles se complexifient.

Quelle est l'alternative ? Il s'agit bien évidemment de construire des abstractions (au sens sémantique et mathématique, pas au sens de "classe abstraite"). Pour ce petit jeu, au lieu d'une approche ontologique, je peux choisir une approche comportementale : je me base alors sur des entités qui échangent des messages (exemple de message : "je me déplace vers telle position", ou "je lance un missile"). Une plus grande complexité implique un plus grand nombre de comportements, mais le lien entre ces comportements est tenu, voir inexistant. La maintenance du produit final en est grandement simplifiée.

L'approche ontologique pure utilisée par les ORM produit elle aussi une architecture qui va, petit à petit, tendre vers le pourrissement. Dans le cas des ORM, ce pourrissement est plus ou moins caché par le fait que le code est généré automatiquement[4]. Mais il est quand même présent, et plus le modèle de donnée va se complexifier, plus vous allez le ressentir profondément.

Point 3 : je débogue. Enfin, j'essaie

Le code produit par les ORM fonctionne comme une boite noire, et nécessite la validation d'un certain nombre de pré-conditions et de post-conditions. Le problème ici est purement lié au développement, mais il faut quand même le prendre en considération : en cas de bug, celui-ci risque fort de ne pas être facile à découvrir (même si le bug est de votre coté et pas dans le code produit). Les messages renvoyés par les frameworks les plus courants sont typiquement peu pratiques et quelque fois difficiles à comprendre.

Ce n'est d'ailleurs pas le seul problème lié au développement. De nombreux utilisateurs d'ORM pointent du doigt les performances des outils qu'ils utilisent. Ces problèmes de performance ont deux sources principales :

  • La complexité du framework lui-même ; de nombreux niveaux d'indirection peuvent tout simplement rendre le code lent à l'exécution.
  • Les requêtes générées ne sont pas optimisées ; sur un produit dont les performances sont directement liées à la vitesse à laquelle les requêtes s'effectue, il faut que celles-ci soient optimisées. Bien souvent, le code produit par les ORM rend plus difficile l'optimisation des accès à la base de données.

Et finalement, bien que le problème soit plus rare, le code généré par l'ORM peut ne pas être du tout celui dont vous avez besoin. C'est ce que Joel Spolsky nomme la loi des abstractions trouées[5] : toutes les abstractions, au delà d'un certain point, sont "trouées" - c'est à dire qu'elles échouent à rendre le service pour lequel elles ont été créées. Si vous rencontrez ce problème, vous mettez les pieds dans un monde nouveau et effrayant ou les clients ne sont pas contents du tout lorsque vous leur expliquez que vous ne pouvez pas faire mieux parce que 1) vous ne pouvez pas changer d'ORM a ce stade du développement 2) votre ORM ne vous permet pas de faire ce que vous voulez. C'est généralement à ce moment là que vous décidez qu'on ne vous y reprendra plus à utiliser cet ORM moisi qu'on vous a forcé à utiliser.

Conclusion

Contrairement à l'approche de certains, je ne considère pas les ORM comme un mal nécessaire. Il existe d'ailleurs des développeurs qui sont d'accord avec moi sur ce point. Quel solution est-ce que je préconise pour éviter le recours à un ORM ?

En fait, aucune. Plus exactement, et puisque je considère que la construction d'abstractions orientées métier est indépendante de la façon dont les données métier peuvent être agencées, je préconise tout simplement l'utilisation d'une approche orientée données pour tout ce qui est du lien avec la base de donnée - c'est à dire que vous écrivez la requête, et vous rangez le résultat de celle-ci comme vous le souhaitez. Le tout sans chichi, sans surcouche inutile, et en acceptant l'idée que vous puissiez manipuler des tables de données. Une approche tout ce qu'il y a de plus naturelle en somme.

Le plus étonnant restant que cette approche naturelle est bien souvent plus simple et plus rapide à mettre en oeuvre et à déboguer[6], qu'elle offre des performances bien souvent meilleures, et qu'elle ne pourrit pas l'architecture de l'application.

En somme, encore un exemple de ce que nous autres, entre gens du monde, appelons le DTST.

Notes

[1] pour information : UML est un langage. En tant que tel, il peut être étendu pour être directement compilable. Executable UML est d'ailleurs un des axes de travail dans le domaine MDA

[2] qu'il faut diviser par deux, une partie des réponses ayant trait à Object Role Model, qui est un poil différent

[3] à ne pas confondre avec une autre forme de stabilité, celle relative au nombre de plantage d'une application

[4] Plus ou moins, parce que certains ORM nécessite des modifications importantes de certains fichiers de configuration - modifications qui peuvent dans certains cas excéder la complexité d'une modification du code généré, ce qui est à la limite de l'absurde.

[5] en anglais, leaky abstraction , ou abstraction qui a une fuite

[6] dixit la version chef de projet de moi même, qui a vu ses collaborateurs passer une demi journée voire une journée à batailler avec un ORM Java très répandu à chaque fois qu'une nouvelle requête devait être implémentée

Commentaires

1. Le vendredi, juillet 30 2010, 16:05 par Ekinox

"Le tout sans chichi, sans surcouche inutile, et en acceptant l'idée que vous puissiez manipuler des tables de données."
Sauf qu'à ce moment-là toute l'application devient dépendante de la base de données : une application qui voudrait modifier une voiture ne peut plus dépendre d'une abstraction quelconque, et si il y a une modification de la table voitures, toutes les parties la modifiant devraient être mises à jour. Avec pour résultat un coût qui me semble plus important que celui de l'ORM.

De plus, tous les ORMs ne génèrent pas du code : certains génèrent la base de données à partir du code, ce qui me semble une bien meilleure idée. De la sorte, on construit d'abord les abstractions de manière ontologique (après tout, une table est une entité, une entité en elle-même n'a pas de comportement), puis l'ORM construit la base de données. Il faut en fait lui donner quelques indications supplémentaires (comment représenter les données dans la BDD, ...), mais en règle générale il repère les variables membres et les stocke dans la BDD. Je pense par exemple à l'ORM que je juge excellent Doctrine2. Il permet même l'héritage entre les tables, que rêver de mieux ?

En fait, j'aimerais savoir en quoi Doctrine2, par exemple, est mauvais pour l'architecture.

Merci d'avance (et merci pour ce blog),
Ekinox

2. Le vendredi, juillet 30 2010, 17:01 par Emmanuel Deloget

Merci pour ce commentaire :)

Sauf qu'à ce moment-là toute l'application devient dépendante de la base de données : une application qui voudrait modifier une voiture ne peut plus dépendre d'une abstraction quelconque, et si il y a une modification de la table voitures, toutes les parties la modifiant devraient être mises à jour. Avec pour résultat un coût qui me semble plus important que celui de l'ORM.

Etant donné que l'ORM, avec son approche ontologique, ne fait que repousser le problème et ne le corrige pas, je soupçonne que modifier une table ait le même impact, que l'on utilise un ORM ou non. La différence se situant alors dans le travail a effectuer pour, en plus, mettre à jour tout les fichiers utilisés par l'ORM pour générer le code (ou autre).

On peut noter que Microsoft a choisi, dans sa vision, de ne pas considérer l'utilisation possible d'un ORM. Ils ont ajouté Linq à C# pour permettre aux développeurs d'écrire des requêtes directement dans le code. Le résultat est plus que plaisant à utiliser - même si, effectivement, une modification du schéma a des chances non négligeable d'entrainer une modification du code un peu partout (mais c'est aussi le cas dès lorsqu'on souhaite exploiter le résultat d'une requête, que ce résultat soit obtenu par un SELECT directement présent dans le code ou qu'il ait été caché par un ORM).

En fait, j'aimerais savoir en quoi Doctrine2, par exemple, est mauvais pour l'architecture.

Je vais me garder de donner un quelconque avis sur Doctrine2, étant donné qu'avant la lecture de ce commentaire, je ne connaissait même pas.

Tel que je comprends le système, la partie ORM de Doctrine2 est double :

  • c'est une surcouche ayant pour but d'abstraire le SGBD, alors que la plupart des autres ORM tente de créer une pseudo-abstraction autour des données elles-même (ce qui n'a pas vraiment de sens ; l'idée de base de la programmation OO est d'encapsuler des comportement, pas des données). En tant que tel, Doctrine2 ORM est une tentative de réécriture de Linq en PHP. Je trouve le concept du query builder très intéressant.
  • c'est un système de génération de schéma de base de données. En termes de maintenance, j'ai vraiment du mal à voir comment ça se passe. Si je dois faire une modification dans le code qui a un impact fort sur le schéma, comment est-ce que je fais pour répercuter ce changement en étant certain de ne pas perdre de données (genre, ma facturation). Si Doctrine2 part du principe que l'ajout de données à la base ne se fait qu'une fois le développement terminé, j'ai bien peur qu'ils aillent au devant de gros problèmes. Rien sur le site web ne montre leur approche fonctionne dans le cas ou je dois mettre à jour une application.

(A noter aussi que je trouve un peu contre-productive l'idée d'un langage de query qui ressemble à du SQL, mais qui n'est pas du SQL, tout en étant tellement proche de ce langage que mes requêtes sont tout aussi complexes).

Après, je ne l'ai pas utilisé (comme je l'ai dit, connait pas). Peut-être que je me trompe, mais il me semble que l'approche de Doctrine2 est plus ou moins vouée à l'échec - malgré de bonnes idées...

3. Le samedi, juillet 31 2010, 12:03 par Ekinox

En fait, selon moi, le problème avec les requêtes SQL est que la simple modification d'un nom de champ qui ne convenait pas pourrait aboutir à la modification partout. Alors qu'un bon ORM permet d'abstraire les attributs sous-jacents de par l'encapsulation.
Pour reprendre l'exemple de Doctrine2 (le seul que je connaisse vraiment tout en trouvant bon), il permet de définir les variables membres comme champs de la table, et ensuite passer par des fonctions pour y accéder. Du coup, on se retrouve avec un ORM qui permet un semblant d'encapsulation. Déjà un point bonus par rapport aux requêtes SQL pures.
Ensuite, pour la partie sur la génération de schéma de la base de données, si il y a une modification du schéma à faire, si je me souviens bien ça passe par les migrations : http://www.doctrine-project.org/pro...
En effet, c'est un projet un peu séparé, mais comme le reste (Doctrine Common/DBAL/ORM/Migrations/...).
Enfin, pour le DQL, il permet simplement de faire du SQL qui ne dépend pas du SGBD. Il offre en plus quelques bonus : join automatique délayé (orthographe ?), ... Après, libre à chacun de préférer query builder ou DQL.

Pour terminer, une petite question : Que penses-tu des ODM ? (et donc des bases de documents) C'est à moitié dans le sujet, mais bon ... Si tu n'as plus d'idées pour les articles, en voici une :)

4. Le lundi, août 2 2010, 15:13 par Brice

Hello Emmanuel, superbes articles ces temps ci!

Je viens du monde Java et effectivement j'ai aussi du mal avec les ORMs, cela dit je ne suis pas tout le temps contre, en effet l'indirection entre le stockage et les instances de ces données (les objets) est un plus dans certains cas. Mais tu as raison de préciser que souvent cette indirection n'est pas aussi bénéfique qu'on le croit, il faut d'ailleurs la maintenir!

En revanche il existe des frameworks, je pense à myBatis, qui permettent de mapper un appel à la base aux objets utilisés dans l'application; cet appel sera soit une requête écrite à la main soit une procédure stockée. Je trouve cette approche plus intéressante car elle permet de faire un vrai travail sur le stockage en étant vraiment isolé de la grappe d'objet, et typiquement si on a un VRAI DBA il est ainsi possible d'engager le dialogue avec lui sur ce sujet.

Autre chose, j'imagine que tu as entendu du mouvement NoSQL. Aujourd'hui il y a plusieurs types de solution NoSQL plus ou moins mûre, en java par exemple il y notamment les graphes avec neo4j, le stockage clé/valeur avec Memcached ou Tokyo Cabinet, le stockage orienté colonne avec HBase (Hadoop).
Dans beaucoup de cas, les bases NoSQL offrent une véritable alternative aux ORM et modèle relationnel, voire même une meilleure solution.

Qu'en penses-tu?

5. Le lundi, août 2 2010, 15:59 par Victor Nicollet

J'ai déjà depuis longtemps épuisé mes arguments contre les ORMs sur mon blog (http://www.nicollet.net/2009/10/het...) mais pour t'en transmettre la substantifique moelle et participer à la conversation:

Je rencontre trop souvent des ORM utilisés pour éviter aux développeurs d'avoir à se "salir" avec le SQL. Du coup, on élimine le besoin d'experts SQL pour réaliser une application, mais on créé à la place un besoin d'experts sur l'ORM considéré. Or, je pense que des compétences PHP+MySQL sont, par exemple, plus portables en termes de nombre de projets que des compétences PHP+Doctrine.

6. Le mardi, août 3 2010, 14:32 par Emmanuel Deloget

@Ekinox
Connait pas les ODM. Ca ressemble à une surcouche. Je vais regarder ça.

@Brice
Hello Emmanuel, superbes articles ces temps ci!
Ca faisait longtemps, hein ? :) C'est la faute à Victor - il m'a redonné le gout de l'écriture avec son blog. En même temps, ça fait du bien de redevenir un blogeur :)

@Brice
Le problème n'est pas tellement les ORM (bien qu'ils favorisent, selon moi, un mauvais design) que ce qu'on nomme impedence mismatch (en gros, le fait que le monde relationnel et le monde objet se marie mal). Il est difficile de s'abstraire de la notion de collection lorsqu'on ne manipule que des collections. Les ORM veulent aller trop loin, encapsuler les tables et les accès aux table, histoire de ne permettre à l'utilisateur que la manipulation... de tables. ?! Donc, au lieu de maintenir le modèle objet + la base de donnée, on se retrouve à maintenir le modèle objet + le code de liaison + la base de donnée. Je crois voire un élément en trop dans cette équation.

Le mouvement NoSQL ne change pas vraiment la donne. En gros, ils disent juste "on simplifie la structure de la base de donnée, donc on simplifie les accès qu'on peut y faire". Mais même masqué, la technologie reste basée sur les fondamentaux de la base de données : tables + requêtes. Le fait de simplifier tout ça fait que dans certains projets, c'est effectivement bénéfique - mais pas parce qu'au niveau design, c'est mieux.

@Ekinox, Brice
l'indirection entre le stockage et les instances de ces données (les objets) est un plus dans certains cas
et
Alors qu'un bon ORM permet d'abstraire les attributs sous-jacents de par l'encapsulation.
L'encapsulation est une bonne chose, si elle est bien faite. Les ORM le font mal - c'est leur défaut.

On reprend à la base, en faisant simple :

1) Qu'est-ce qu'une table ?

  • C'est une collection de données.
  • En tant que telle, aucun comportement ne lui est associée.

2) Qu'est-ce qu'un enregistrement ?

  • C'est un N-uplet
  • Un N-uplet n'a pas de comportement associé.

Donc, ni la table, ni l'enregistrement ne sont des objets. Prétendre le contraire, c'est violenter sans vergogne le concept de base de données.

3) qu'est-ce qu'une requête ?

  • A priori, c'est une fonction qui associe des conditions à un ensemble de tables, pour en dériver une table résultante (le résultat de la requête ; cette table peut ne contenir qu'un seul enregistrement, voire une seule donnée ; mais c'est une table quand même). Une procédure stockée fonctionne de la même manière - c'est une requête, souvent un peu plus complexe qu'un simple statement SELECT.
  • Vu que ni les tables ni les enregistrements n'ont de comportement, la requête ne peut leur être associés. Tables et enregistrements sont des paramètres de la requête.

La conclusion : les requêtes devraient être implémentées en tant que fonctions libres (désolé pour les programmeurs Java ou C# ; on vous avait dit que votre langage avait quelques soucis avec des concepts simples... Bon, on peut avoir un résultat similaire en les implémentant sous la forme de fonctions statiques).

Le corolaire de cette conclusion : tout système qui prétend encapsuler les tables, les enregistrements ou les résultats de requêtes dans des objets se trompe lourdement - quelle que soit le sens dans lequel ce système travaille.

Faut-il pour autant refuser l'encapsulation ? Pour sûr, non ! Rien ne m'empêche en effet de créer (par exemple) une fonction par requête. Dans la pratique, c'est ce que je vais faire. La surcouche sera nécessairement légère, donc relativement simple à maintenir. Les tables sont représentées par des collections, les enregistrements par des structures. La couche de liaison n'abstrait pas le modèle de données que j'utilise, mais le SGBD.

@Victor
C'est pas faux. C'est même plutôt vrai, en fait. Et ça pose un problème supplémentaire lorsqu'il faut optimiser une requête : le spécialiste d'un ORM aura plus de problème à effectuer cette tâche que le spécialiste SQL.

7. Le vendredi, août 6 2010, 16:01 par Brice

@Victor
Je comprends vraiment les développeurs qui ont du mal avec le SQL, c'est une manière de pensée différente. L'orientation "donnée" n'est pas aussi simple que ça, il y a les notions de volume, l'approche SQL différente. Il y a un défaut de compréhension dans la manière d'utiliser SQL, les base de données et dans l'appréhension d'un problème. Et c'est normal vu la manière dont on nous apprends les choses à l'école, et dans la vie professionnelle (vu le nombre de projet mal foutu ou il n'y a quasiment des débutants (et ce n'est pas leur faute)).
Bref le SQL ce n'est aussi simple que ça, mais je suis d'accord, ce serait bien de faire un effort. Typiquement je suis pour la programmation polyglotte : Choose the right tool for the right job.

@Emmanuel
Oui je comprends bien ton point de vue sur cette notion forte de donnée, et pas de comportement en base. C'est typiquement une question de responsabilité, l'application permet de travailler sur les données en appliquant des comportements sur celle-ci, alors que la base de données doit uniquement gérer ces données.

A vrai dire en écrivant cette réponse je comprends même mieux ton point de vue, ce n'est clairement pas les même responsabilités, les même problèmes ou les mêmes approches pour résoudre ces problèmes.
Ceci dit, les données en base sont bien organisées dans des tables, dans des colonnes, éventuellement dans des structures.

Les fonctions libres, d'accord, à vrai dire je m'intéresse à la programmation fonctionnelle (comme Scala ou Erlang), mais ces fonctions libres ne peuvent pas s'appliquer à toutes les données pour toutes les tables, typiquement je pense qu'il y a un couplage inhérent entre le données et le comportement. Dans les faits quand on écrit une requête SQL, on travaille directement depuis/sur ces données, et la manière d'écrire la requête peut changer suivant le contexte.

@Emmanuel, Ekinox
"Alors qu'un bon ORM permet d'abstraire les attributs sous-jacents de par l'encapsulation."
En ce qui concerne l'encapsulation, je ne suis pas sur de bien comprendre ce que vous dites et sur quoi s'applique la dite encapsulation. En ce qui me concerne je la situe uniquement coté applicatif, relative au langage utilisé et bien entendue relative à l'API de nos objets.

Par exemple en créant des objets immuables ou simplement des objets sans setter, qui seront donc valides/corrects au niveau métier, pourra protéger cet objet d'être mal utilisé par un développeur. Et en Java par exemple il y a moyen d'accéder directement au champs de cet objets, en assumant que les données en provenance ou en direction de la base sont conformes aux exigences métier.

Un ORM n'encapsule pas des objets à la base il abstrait le code technique relatif à la base, ce n'est pas la même chose. Après effectivement le principe n'est pas particulièrement adapté pour une "relation" objet-relationnel. Mais il ne faut pas non plus oublier la notion de "Mapping", qui n'indique pas forcement la notion de table physique en base.
Je pense en particulier à myBatis qui permet de vraiment travailler avec du SQL.

@Emmanuel
Pour finir, NoSQL change la manière d'appréhender le problème de stockage, typiquement suivant les besoins il n'y aura pas besoin de travailler sur les données de la même manière. Pourquoi prendre une base de données si les besoins ne correspondent manifestement pas. NoSQL englobe des paradigmes assez différents (comme je le disais plus haut, il y a des graphes, des systèmes clés/valeurs, documents, etc.). Après ça n'empêche pas aussi de faire du mauvais design avec du NoSQL. Encore une fois "Choise the right tool for the right job".
Et pour info, j'ai même vu un article sur le YeSQL.

Mais ce qu'il faut retenir c'est qu'en prenant en compte le véritable besoin, on supprime le problème introduit par le choix facile de une base une application (comme LAMP), pourquoi choisir du SQL tout le temps si ce n'est pas nécessaire. J'ai envie de dire que tant qu'il n'y a pas de vrai justification à utiliser quelque chose, autant l'éviter (dans la mesure du possible).

En tous cas c'est cool de discuter de ça :)
Ça devrait se retrouver sur developpez.com, d'ailleurs il devrait y avoir un annuaire de blog. Ou une revue de blog.

8. Le lundi, août 9 2010, 16:32 par Emmanuel Deloget

@Brice
OK pour le concept de NoSQL. OK aussi pour ne pas utiliser de manière systématique une base SQL lorsqu'on fait une application: ce n'est pas forcément le plus adapté. En pratique, on utilise une base de données à partir du moment où on a entrevu le besoin d'une base de données. Et on utilise une base SQL à partir du moment ou l'on a établi qu'il y avait des relations particulières, complexes, entre les données qu'on souhaite stocker. Si ces relations sont simples, un système simplifié peut très bien faire l'affaire.

Sur l'encapsulation: j'ai coutume de dire "on encapsule pas des données, on encapsule des comportements", axiome qui est à la base des autres principes OO majeurs, et dont dérive aussi certaines règles de style comme la loi de Demeter (qu'on l'aime ou non). Des données brutes, si elles ne sont pas liées à des comportements, n'ont aucune raison d'être encapsulées. Par contre, une requête qu'un n'est autre qu'un comportement du type "j'ai besoin de telle et telle chose, en fonction de tel ou tel paramètre" peut tout à fait être encapsulé - sous la forme d'une fonction.

C'est ce que j'entends pas encapsulation dans ce contexte spécifique: l'application stricte de l'axiome évoqué ci-dessus. Si un ORM applique ce principe, on aboutit à un code simplissime. Qu'on rajoute des comportements aux données (ou aux n-tuples, voire aux tables) et on obtient nécessairement quelque chose qui n'est plus logique - en termes de respect des principes de POO - et posant plusieurs problèmes de maintenance (par exemple, soyons naïf : si le seul fait de mettre à jour une donnée provoque un accès à la base de données, alors j'ai un problème de performances potentiel sur les bras, et je ne sais pas m'en défaire). Si le mapping a pour effet de proposer une interface entre mon application et le modèle de donnée qui n'est pas liée à ce modèle de données, alors je ne contrôle plus rien sur la manière dont les données vont être traitées. Je suis obligé de laisser l'ORM proposer sa propre sauce interne, et je me retrouve de nouveau avec un potentiel problème de performances. Une partie de mon code est maintenant une boite noire dont je ne gère pas les fonctionnalités - ce qui est quand même cocasse, vu que je suis censé les définir.

Donc, pour résumer : une encapsulation trop forte qui respecte le modèle de données n'est pas adéquate car elle ne respectera pas les principes de POO ; et une encapsulation qui respecterait un modèle objet valide et les principes de POO n'est pas adéquate non plus car elle ne respectera pas mes besoins. D'où la nécessite de réaliser une encapsulation légère - quitte à consolider celle-ci grâce à un adapater.

9. Le mercredi, août 11 2010, 13:36 par Ekinox

@Victor> Sauf que souvent, à la fois l'ORM est plus simple à apprendre que le SQL (j'ai mis 3 jours à comprendre Doctrine, un mois à comprendre les jointures ... Le fait d'avoir appris l'un avant l'autre a peut-être aussi faussé la donne.). De plus, je trouve, au moins personnellement, que le code utilisant l'ORM est bien plus concis qu'une requête suivie du mapping manuel à un objet (cf. la suite sur ce point).

@Emmanuel>
ODM> Ce n'est pas tellement une surcouche, si j'ai bien compris. C'est un système de pensée différent, proche en ce point du NoSQL (des fois, je me demande même si ce n'est pas une sorte de NoSQL ...).
Encapsulation des comportements>

  • La table est une collection. Or une collection doit disposer de méthodes permettant de récupérer certains de ses éléments, en les triant ou pas. D'où la représentation objet qui me semble logique.
  • Un enregistrement n'est pas forcément un N-Uplet. C'est une entité. Ce que tu dis revient à dire que la sérialisation c'est le mal, que ce qui est sérialisé et désérialisé devrait être uniquement des N-Uplets. Or une Personne peut être sérialisée puis désérialisée, elle devrait toujours disposer de sa méthode direBonjour. Un enregistrement d'une base de données, du point de vue des ORM, est une sérialisation. D'où la logique de placer des objets. Si, au contraire, la table n'a qu'une valeur de N-Uplet, alors, oui, il faudra faire mapper ça par l'ORM en une simple structure, voire ne pas le faire mapper.
  • Une requête ne devrait pas être gérée par une fonction, du point de vue de certains ORM. Elle devrait être gérée en transparence : la modification d'un objet géré par l'entity manager va automatiquement cascader en un update. Par contre, l'ajout et la suppression d'objets doivent être explicitement indiqués, pour éviter les accidents. Il n'y a donc plus de problèmes de requêtes.

Optimisation> Si on utilise un ORM, ce n'est pas pour avoir besoin d'optimiser. Un ORM ira très probablement toujours plus lentement que du code PHP/SQL écrit à la main. De plus, dans certains cas, les requêtes partielles sont possibles pour optimiser (par exemple avec le DQL dont la syntaxe est quasiment identique au SQL, peut-être avec le Query Builder, que j'ai moins étudié). L'optimisation plus profonde, déjà complexe en SQL, est infaisable avec un ORM. Mais, si on désire ce niveau d'optimisation (contrôle de chaque requête, ...), autant coder directement en PHP/SQL (j'ai toujours pris l'exemple du site web, le plus commun dans mon expérience). Comme dit plus haut, si on veut un ORM, c'est pour coder plus vite, pas pour que le programme aille plus vite.

@Brice>
"Un ORM n'encapsule pas des objets à la base il abstrait le code technique relatif à la base, ce n'est pas la même chose."> ORM = Object Relational Mapping. Donc ce n'est pas uniquement ce qui abstrait le rôle relatif au SGBD. C'est le rôle de quelque chose entre l'ORM et PDO, par exemple Doctrine DBAL (toujours les mêmes exemples, ceux que je connais le mieux).

10. Le dimanche, août 22 2010, 02:00 par Brice

@Ekinox
Oui j'ai été un peu rapide sur ce que je disais, je voulais dire : "Un ORM n'encapsule pas des objets dans la base, mais il abstrait le code technique relatif au mapping de ces objets par rapport à la topologie de la base". En PHP je ne m'y connais pas vraiment. Mais en Java, un ORM comme Hibernate ou Toplink (maintenant EclipseLink) fait direcvtement appel à la brique JDBC qui fournit quelques abstractions sur le code technique relatif à la connectivité de la base.
A coté de ça cette brique JDBC fournit des moyens des abstractions sur les connections et les transaction (avec une architecture type JCA).

@Emmanuel
Ok je vois ce que tu veux dire. Pour le coup j'ai l'impression de retrouver ce que tu dis dans les principes de l'approche DDD (Domain Dirven Design). Ou le code est fortement orienté objet, et effectivement on peut y appliquer les bons principes objets comme ceux de Uncle Bob ou la loi de Demeter. Cela dit il éxiste des entités qui font partie du domaine, mais qui ne peuvent pas vraiment faire partie d'une grappe d'objet, c'est ce qu'on appelle des services. C'est services prennent donc des paramètres en entrées et retourne ou pas quelque chose.

En DDD même, il y a aussi la notion de Repository, d'Entité, de Value-Object. Je dérive aussi, mais l'approche DDD permet de rationaliser la manière de penser notre application et comment elle fonctionne avec les systèmes extérieurs, typiquement une base de données. A l'intérieur d'une applications tout les objets, ou plutôt toutes la logique métier n'a pas besoin de fonctionner sur les mêmes paradigmes.
Une sauvegarde basique, pourra être intéressante avec un ORM. Par contre si on veut faire quelque chose plus complexe alors un service (ou une fonction comme tu le dis) avec une implémentation adaptée pourrait être plus appropriée.

11. Le mardi, août 24 2010, 20:26 par Washu Hakubi

Bit late, but... Biggest problem I see is that Relational databases and Objects don't tend to go hand in hand. If you design your database to house objects, then its typically an inefficient database design (for RDBMS), and if you design your objects around your database schema you tend to have the opposite problem.

Its amusing how many classic examples we use to justify ORMs, a classic one being the Person/Address/Telephone example, and how those all just sort of fit together. Its such a pity that real life tends not to flow that nicely.

I've also found that most ORMs don't tend to adapt well across many different database providers. They'll typically be good for one or two, usually one of the big three, but when you're in the business world you tend to not just be using one of those, but a few other database types that might not be as popular (or even well known at all), which may or maynot support the typical SQL shenanigans that something like Microsoft SQL Server or Oracle does. This leads to either huge performance hits versus customized queries, or having to write half the queries yourself anyways.

The last problem I've found is that many people will strongly couple with their ORM generated objects, which makes changing your database or mapping schemas almost impossible. Instead of these just being objects for storing and retrieving data they end up being core components of your business layer. Logic that should be abstracted away, or in another module will often times be dropped onto ORM objects, or coupled strongly to those objects just because its "easier" that way (at first).

12. Le jeudi, septembre 9 2010, 00:48 par Christophe Quintard

Bonjour,
cela me fait plaisir de voir que je ne suis pas le seul à n'avoir pas succombé aux ORM.
J'ai découvert pour ma part deux raisons pour expliquer l'engouement pour les ORMs, tout comme l'engouement pour les J2EE, Struts, Hibernate, Spring et autre couches et surcouches qui remplacent le code par de la configuration. Et ce ne sont pas des raisons basées sur des choix techniques ! La première raison pour laquelle certains développeurs utilisent un framework est tout simplement qu'ils ne sauraient pas faire sans. La deuxième raison est que les SSIIs utilisent ces frameworks afin de démontrer leur maîtrise technique à leurs clients (toute ma sympathie pour les pauvres développeurs qui doivent assumer derrière).

Je suis également de l'avis qu'une dizaine de lignes de code valent mieux qu'une ligne de configuration. Il est effectivement plus facile de déboguer et d'optimiser du code que de la configuration. Ensuite il est plus facile de trouver des experts en programmation que des experts en tel ou tel framework. Il y aura toujours des développeurs C++ ou Java dans 5 ans pour la maintenance du projet, mais trouvera-t-on encore des experts de l'ORM utilisé ?

Pour ma part, je pense que pour détourner les gens des ORMs, le mieux est encore de leur expliquer comment s'en passer. Ayant été confronté au problème plusieurs fois, j'ai connu quelques échecs avant d'aboutir à l'approche suivante.

Voici comment je procède :
1) Implémentation du modèle de données
Je commence par implémenter un modèle de données classique avec des get/set ou des add/remove et des listeners. A ce stade, je peux déjà utiliser mon modèle, mais il n'y a aucun backup. J'ajoute ensuite à toutes les classes du modèle la possibilité d'enregistrer des hooks sur chaque méthode modifiant le modèle (set, add et remove). Un hook est une méthode qui est appelée avant que les modifications ne soit effectuées, et si le hook ne retourne pas "true", alors les modifications ne sont pas effectuées.

2) Implémentation de la solution de backup : mise à jour
Dans le module de backup, j'écris une première classe dont le constructeur prend en paramètres une référence sur un modèle et une référence sur un gestionnaire d'erreurs (un error handler). Cette classe va enregistrer des hooks sur tous les objets du modèle. Ainsi, chaque fois qu'une modification sera tentée sur le modèle, un hook de notre module de backup sera appelé avant. Lorsqu'un hook est appelé, il tente de reporter la modification dans la base de données. Si la modification réussit, le hook retourne "true" et le modèle est modifié aussi. Si la modification en base de données échoue, alors le hook retourne "false" pour que la modification du modèle soit abandonnée également, et il notifie le gestionnaire d'erreurs.

3) Implémentation de la solution de backup : lecture initiale du modèle
Dans le module de backup, j'écris une deuxième classe chargée de lire la base de données afin de construire un modèle de données correspondant.

4) Branchement de la solution de backup
Il suffit d'utiliser la classe de lecture de la base de données pour créer le modèle, puis de référencer ce modèle dans un objet qui fera la mise à jour. Le gestionnaire d'erreurs est branché dans la console de l'application, ou bien déclenche l'apparition de boîtes de dialogue pour indiquer les erreurs survenant avec la base de données.

Avantage de cette approche :
- le modèle de données reste complètement indépendant du backup en base de données. Si vous décidez de changer pour un fichier XML, il suffira de remplacer le module de backup en base de données par un module de backup en fichier XML, il ne sera pas nécessaire de modifier une ligne de votre modèle de données. Cela limite fortement l'impact sur le reste du code.
- les erreurs SQL sont toutes remontées par un canal unique, le modèle de données n'en est pas pollué.
- le branchement du module de backup en base de données se fait en quelques lignes : une pour lire le modèle de données depuis la base de données, une pour enregistrer le modèle de données auprès de la classe qui s'occupe de la mise à jour de la base de données, et enfin quelques unes pour brancher le gestionnaire d'erreurs. Il est donc facile de changer de solution de backup si on le souhaite.

On m'a déjà avancé l'argument qu'un ORM a l'avantage de permettre de passer d'une base de données à une autre simplement. Bien que d'un intérêt limité, car je connais peu d'applications supportant plusieurs bases de données, je propose néanmoins la solution suivante pour en faire de même avec une approche sans ORMs : utiliser des catalogues de messages pour stocker toutes les requêtes SQL. Après tout, si les catalogues de messages permettent à votre application de parler anglais, français, espagnol ou chinois, ils peuvent aussi lui permettre de parler Oracle, MS SQL ou DB2.

13. Le samedi, septembre 22 2012, 19:07 par Thomas

Bonjour,

Article très intéressant que je viens de lire !

Je suis aussi d'avis que les ORMs créent plus de problèmes qu'autre chose au final ou sont à utiliser avec intelligence. Etant sorti de l'école à la jonction de la période "old school" et "nouvelles technologies" (2006 environ), je dois avouer que les développeurs issus des "nouvelles technologies" (Struts, EntityFramework, XAML, ...) n'ont plus trop la même logique en terme de développement. Ils préfèrent se prendre la tête à debugger un code EntityFramework complexe (enfin... quelques jointures et éventuellement sous requêtes ^^) alors qu'une simple procédure stockée SQL résoudrait le problème en moins de temps et garantirait un meilleur résultat (qualité et performance)...

Au final, je ne pense pas qu'un ORM permette de coder plus vite (sortir une fonctionnalité ou accélérer la productivité dans un projet, cqfd), car à moins d'être un expert dans le-dit ORM (qui prend du temps car il y a pas mal de subtilités et best pratices à connaitre avant bien maitriser), il y aura généralement un soucis avec une requête (performance ou comportement bizarre suivant le métier par exemple) ; suivant la complexité des requêtes bien entendu...

J'ai tenté d'utiliser Doctrine dans un projet perso PHP. Je l'ai abandonné (doctrine pas le projet ^^), dès que je me suis aperçus qu'il me chargeait en mémoire des données dont je n'avais absolument pas besoin : lorsqu'on utilise un cache ou une session, on doit toujours faire attention à la taille des données que l'on souhaite conserver pour ne pas exploser le serveur... Règle de base dans la programmation web...
Du coup, j'ai fait ma propre couche d'accès aux données. Au final, j'ai quasi la même chose que l'ORM (en moins poussé techniquement bien entendu) : des objets et un accès à leurs relations sous forme de méthodes... C'est "un peu" plus long à faire, mais franchement plus simple à débogger ; surtout avec les messages d'erreur que renvoie Doctrine lol...

Une dernière chose à bien différencier : architecture et ORM.
Les ORMs n'empêchent pas de faire une architecture projet cohérente : on peut faire du MVC ou N-Tier propre sans ORM... l'ORM est indépendant de l'architecture utilisée.

@Christophe :
L'argumentation que l'on vous a donné concernant le passage à une autre base de données est bonne en fait, car étant donné que l'ORM est une couche d'accès aux données et qu'elle renvoie des objets, ils sont indépendants de la base de données. Donc il est facile de connecter une autre source de données (pour des tests unitaires par exemple), sans devoir modifier les objets utilisés dans les couches supérieures. Mais comme je l'ai dis juste au dessus, une architecture "propre" possède déjà sa couche d'accès aux données (ORM ou pas).

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet