Base de données, objet et accès aux tables

Un collègue m’a interrogé récemment sur la manière d’utiliser les objets en PHP. Programmant depuis quelques années, il a découvert les concepts objets ces derniers temps. Il a commencé à utiliser des outils de type ORM (Object Relationnal Mapping), qui facilitent le couplage entre une classe et une table de la base de données. Sa question était la suivante :

« Je développe une application de gestion de dossiers. Dans ma base de données, j’ai une table Dossier, qui est reliée à une table Bénéficiaire, par l’intermédiaire d’une table me permettant de définir bénéficiaires pour le même dossier (relation de type n-n). De plus, un bénéficiaire peut avoir plusieurs adresses. Voici une représentation de la base de données :

J’ai créé des classes de type ORM, qui me permettent de mettre à jour ma base de données, mais je rencontre un problème. J’aimerai faire une recherche de dossiers par code postal des adresses de type « Siège ». Faut-il que j’instancie toutes les classes et que je les combine pour obtenir mon résultat, ou est-il préférable de créer une requête SQL qui va me restituer l’information directement ? D’un point de vue conceptuel, qu’est-ce qui est le mieux ? ».

Pour répondre à une telle question, nous allons successivement étudier :

  • le principe de fonctionnement des bases de données ;
  • ce qu’est la programmation objet, et surtout, à quoi elle sert ;
  • les encapsulations d’accès aux données fournies par les classes de type ORM.

Principe de fonctionnement des bases de données

Les bases de données ont été inventées à une époque où l’informatique était nettement moins puissante qu’aujourd’hui. Le maître-mot, c’était : efficacité. Il fallait, avec des matériels quasiment dépourvus de mémoire et d’espace disque, obtenir des résultats rapidement et stocker de l’information dans un espace qui était limité. C’est ce qui a conduit à des optimisations particulièrement puissantes, dont nous héritons aujourd’hui : il existe de nombreux types de données, avec en plus, des espaces de stockages variables à l’intérieur d’un même type ; par exemple, pour un entier, (integer), le stockage peut s’envisager sur un octet (tinyInt) jusqu’à plusieurs 8 ou 16 octets pour les plus volumineux. Des mécanismes particulièrement performants ont été inventés pour récupérer, manipuler et assurer la cohérence informations. Petit à petit, ce sont les bases de données de type relationnel qui se sont imposées, associées, au langage SQL, au détriment d’autres approches, dont certaines étaient également très performantes. Et même si elles sont parfois contestées pour certains usages, notamment dans le cadre de très grosses bases de données réparties (informatique en nuage), on utilise essentiellement des technologies créées il y a plusieurs décennies.

Dans le cas qui nous préoccupe, c’est le mode d’échange des informations entre l’application et le SGBD qui nous intéresse. Le SGBD est implémenté dans un serveur, et l’application accède au SGBD en lui transmettant des requêtes selon le protocole TCP/IP. Voici un schéma récapitulant ce fonctionnement :

Dans le poste de travail (ou dans le serveur hébergeant l’application), est installé un logiciel, le client SGBD, qui va gérer le dialogue avec le logiciel SGBD. L’application transmet donc toutes ses requêtes à ce client.

Pour chaque requête, celui-ci va préparer des trames TCP/IP, qui contiendront :

  • l’adresse IP du serveur hébergeant le SGBD ;
  • le port TCP qui est écouté par le SGBD, par exemple 5000 pour MySql ;
  • l’adresse IP du client ;
  • le port TCP du client, qui va écouter la réponse. Ce port est choisi aléatoirement par la couche TCP ;
  • et, bien sûr, le contenu de la requête à adresser au SGBD.

Le programme de gestion du réseau va s’occuper d’acheminer les trames vers le serveur. Ce dernier va récupérer la requête, puis la transmettre à l’application qui est en écoute sur le port TCP ; cette application est notre SGBD. Celui-ci va alors exécuter la requête, puis va en transmettre le résultat au client, dont il connaît l’adresse IP (elle figurait dans la trame de demande de requête) et le port TCP.

L’ordinateur du client récupère la trame générée par le SGBD, la transmet à l’application qui est en écoute sur le port TCP fourni initialement. Le client SGBD remet en forme les informations transmises par le SGBD, puis les fournit à l’application, qui peut les exploiter.

Ce qu’il faut retenir de cette phase, c’est que le SGBD exécute une requête et fournit une réponse. Pour les SGBD SQL, la requête est une requête SQL, et la réponse va contenir plusieurs informations : un code de retour, un code d’erreur, et le résultat sous forme d’un ensemble de lignes (dans la pratique, on utilise un mécanisme de curseurs pour récupérer les différentes lignes de la requête ; néanmoins, les langages modernes cachent cette complexité au programmeur).

Il est possible de représenter le mécanisme à partir d’un diagramme de séquence UML :

Au final, l’application ne fait qu’utiliser une seule fonction du client, la commande execute, avec comme seul argument le code SQL à exécuter.

A quoi sert la programmation objet ?

Les premiers programmes étaient une suite d’instructions directement interprétées par le processeur. Le premier langage était l’assembleur, qui permettait d’indiquer au processeur ce qu’il devait faire, et comment. Plus performant en terme de vitesse d’exécution, il n’y a pas ! Mais pour programmer, c’est autre chose... Très vite, on est passé au langage C, un compilateur permettant de transformer le code source en code machine, les performances restant tout à fait honorables, voire très proches de l’assembleur. [1]

Les inconvénients de cette programmation, également appelée procédurale, se sont vite fait sentir : code incompréhensible, codage multiple de la même fonction, selon des modalités différentes, à divers endroits du programme, débogage fastidieux... Les coûts de développement explosent, et il est urgent de trouver des approches moins coûteuses. C’est le concept objet qui va apporter la solution.

Le principe de la programmation en mode objet tient en peu de mots : coder une seule fois, valider le code créé, et l’utiliser partout où il est nécessaire. L’utilisation de fonctions aurait pu faire l’affaire, mais elles se sont vite révélées insuffisantes : pour manipuler certains types d’informations, il était nécessaire de disposer de plusieurs fonctions différentes, et il fallait conserver, entre chaque, les états des variables (les valeurs). D’où la création d’objets, dont la représentation informatique s’appuie sur un modèle (la classe) et une instanciation (son environnement d’exécution). Je passe sur les notions plus complexes, comme l’héritage, qui permet, à partir d’une classe initiale, de créer des classes dérivées avec des propriétés distinctes. Ce n’est pas l’essentiel de mon propos.

L’utilisation de la programmation objet et des classes présente énormément d’avantages : le code n’est écrit qu’une fois, il est facile à tester, et une fois que la classe est validée, elle peut être utilisée (presque) partout _ [2]. Toute modification de la classe se répercute partout dans l’application, la maintenance et les évolutions en sont facilités.

Mais l’utilisation des classes n’est pas la panacée. En particulier, elles nécessitent beaucoup plus de ressources lors de leur exécution. Prenons un exemple, écrit en PHP :

<?php
$a = 5 ;
$b  = 7;
echo ($a * $b);
?>

Rien de bien transcendant, n’est-ce pas ?

Maintenant, la même chose avec l’utilisation d’une classe :

<?php
class Calcul {
        function multiplication ($a, $b) {
                return ($a * $b);
        }
}
// Instanciation de la classe
$calcul = new Calcul();
echo ($calcul->multiplication( 5,7));
?>

L’exécution de ce deuxième script sera plus long, quoi qu’on en dise... D’ailleurs, de nombreuses sociétés confrontées à des accès massifs à des sites internet font très attention à l’utilisation des classes, et parfois évitent d’en programmer au profit d’un codage classique. Cela permet d’éviter de trop dégrader les performances et, au final, cela coûte moins cher ; plus l’application est lente, plus il faudra de matériels pour gérer un grand nombre de connexions.

Mais revenons à nos moutons... Un des avantages majeurs de l’utilisation des classes, c’est de réduire le temps de développement, et donc le nombre de lignes de codes produites par un développeur pour réaliser une opération. Et s’il y a bien des opérations chronophages en terme de codage, ce sont les accès aux bases de données SQL !

Le langage SQL est particulièrement difficile à coder. Tant qu’on tape une commande dans un terminal, on ne s’en rend pas bien compte. Mais dès lors que nous allons mettre ça dans un script, cela devient vite compliqué... Parmi les difficultés :

  • la syntaxe est différente selon les types de données. Une variable de type texte nécessite des guillemets pour l’encadrer, un nombre non ;
  • dans le même ordre d’idées, les dates sont stockées en base de données selon un format qui est différent du format utilisé en français ;
  • les commandes d’écriture diffèrent selon qu’il s’agisse de l’insertion d’une nouvelle information, ou d’une modification.
    • Pour pallier ces inconvénients, des classes ont été développées, qui permettent de masquer ce codage un peu fastidieux. On les appelle ORM (Object Relationnal Mapping), ou couplage relationnel-objet. Derrière ce vocable pompeux se cachent simplement des mécanismes qui permettent de générer automatiquement les requêtes SQL de lecture et de modification des enregistrements d’une table. Pour réaliser ces opérations, ces ORM s’appuient sur des descriptions des tables ou de la base de données, établies selon diverses modalités : commentaires dans le corps de la classe héritée, fichiers XML, tableaux associatifs... Les plus évolués sont capable de gérer les relations entre tables, et notamment les contraintes d’intégrité.

Pour les utiliser, dans la pratique, il faut créer une classe héritée pour chaque table de la base de données. Par rapport à notre exemple précédent, nous nous retrouverions avec ce schéma :

A chaque table de notre base de données correspond une classe héritée de l’ORM (ici ObjetBDD [3], un ORM parmi d’autres).

Ce schéma ressemble fortement au schéma de la base de données, et pour cause : on y retrouve les mêmes noms, qu’ils soient des noms de classes ou de tables ! Mais ils sont radicalement différents. Le rôle de la base de données, c’est de stocker les informations, de les organiser, et de les conserver en cohérence. Ces classes, que j’appelle « techniques », servent uniquement à lire et à écrire rapidement vers la base de données. Il serait tout à fait possible, plutôt que de créer des classes héritées de l’ORM, de n’en créer que des instances, en indiquant dans chaque instance les paramètres nécessaires (nom de la table, colonnes, types de données...). Mais ce serait fastidieux, surtout si les tables sont utilisées à des endroits différents dans l’application. En général, il est préférable de créer une de ces classes techniques pour chaque table de la base de données.

Comment résoudre le problème initial ?

Maintenant que nous avons fait un petit détour parmi ces différents concepts, voyons comment résoudre notre problème. Que veut-on faire ? Nous voulons extraire une liste de dossiers à partir du code postal de l’adresse du siège social du bénéficiaire. Il s’agit bien d’une liste de dossiers : c’est une information qui va être gérée à partir de notre classe Dossier.

La classe Dossier est héritée de notre ORM ? La belle affaire ! Les fonctions natives permettent de lire et d’écrire un enregistrement, ce sont effectivement des fonctions dont nous avons besoin. Mais nous avons également besoin d’obtenir une liste des dossiers, selon des critères bien spécifiques.

D’un point de vue pratique, nous allons demander à la base de données de nous fournir cette liste. Souvenez-vous : la base de données ne comprend qu’une commande, la commandeexecute($sql).

Nous avons donc besoin de créer une nouvelle fonction pour notre classe Dossier, qui contiendra les lignes suivantes :

function getListeDossierCPAdresseSiege($param) {
$sql = 'select * 
from Dossier
left join DossierBeneficiaire (…)
left join Beneficiaire (…)
left join Adresse (…)
where Adresse.TypeAdresse_IdTypeAdresse = '.$param;
return this->execute($sql);
}

J’ai simplifié largement et le code SQL, et la partie concernant l’exécution de la requête SQL. En général, les ORM sont couplés à d’autres classes qui gèrent uniquement la relation à la base de données (identification, mise en forme des informations...). Parmi ces classes, les plus connues en PHP sont ADOdb, PDO, PEAR:MDB2. Mais c’est un autre sujet...

En résumé...

Pour résoudre notre problème, nous avons créé une nouvelle fonction dans notre classe, qui contient un code SQL adapté à ce que nous voulons. Nous n’avons pas interrogé toutes les classes intermédiaires (les classes techniques utilisées pour stocker les informations dans la base de données), ce qui aurait été particulièrement fastidieux et peu performant.

Quelques conseils :

  • ne confondez pas le modèle relationnel, utilisé dans les bases de données, et le modèle objet, utilisé pour décrire le comportement des applications ;
  • méfiez-vous de la représentation des classes techniques de type ORM, qui risquent de vous masquer le fonctionnement que vous attendez de votre application, en y plaquant un schéma relationnel ;
  • faites simple ! Cela me rappelle un des 12 commandements du Manifeste Agile : « La simplicité ­ l’art de maximiser la quantité de travail à ne pas faire ­ est essentielle » . Un code d’une grande simplicité apparente est souvent le résultat d’années de pratique... Suivez votre intuition, et ne cherchez pas à faire « savant » !

[1] Il est possible que je dise une bêtise, ce n’est pas vraiment ma partie, je n’ai fait qu’effleurer l’assembleur, et je n’ai jamais programmé en langage C...

[2] A ce propos, il est important de noter qu’une classe s’exécute dans un cadre, notamment les classes de calcul. Il me semble bien qu’une des premières fusées Ariane 4 avait raté son décollage à cause de la réutilisation d’une fonction de calcul dont les bornes d’entrée étaient incompatibles avec les informations fournies par la navette...

[3] http://objetbdd.sourceforge.net/