C'est l'histoire d'un hacker à qui on a demandé de créer des pages web pour le suivi de la production logicielle de la société. Historiquement, toutes les informations étaient gérées à la main, ce qui est une solution inacceptable pour tout hacker qui se respecte.
Le but de cette série d'articles est de vous montrer comment on peut utiliser Perl, MySQL, phpMyAdmin, les modules DBIx::Class, DBIx::Class::Schema::Loader et HTML::Tiny pour gérer ces informations d'une manière plus efficace. Ces articles vont donner une vision d'ensemble sur une solution mettant en jeu plusieurs technologies au lieu de se focaliser sur un domaine bien précis.
Ce premier article va expliquer les besoins de l'application, la structure de la base de données et comment créer cette structure dans une base MySQL avec phpMyAdmin. Au passage, on verra les principes de base des relations entre les tables et l'utilisation des index et des clés étrangères (foreign keys) pour que MySQL assure la cohérence des entités de l'application.
1. Introduction
Le problème à résoudre : comme toute société informatique qui se respecte, nous produisons plusieurs produits logiciels. Chaque produit évolue et cette évolution est traduite en termes de version. Et comme dans toute société, les noms de code et numéros de version utilisés pendant le développement correspondent rarement avec ceux décidés (tardivement) par le marketing. Devant cette profusion de noms de codes et de versions, il est difficile de s'y retrouver. J'ai donc été chargé d'écrire une application qui fournit des pages d'information résumant la situation.
Donc, pour générer ces pages, il faudra gérer la correspondance entre :
- un produit tel qu'il est vu par le marketing (et par le client) avec :
-- un nom de produit en général évocateur de simplicité et de performances pour appâter le client,
-- quelques informations supplémentaires (release notes) ;
- une version de produit dont le numéro est en général décidé tard dans le cycle de développement ;
- Cette version de produit est déclinée en fonction de l'environnement cible : distribution (RHEL 5 ou RHEL 6) et architecture (i386 ou x86_64) et est livrée dans un fichier tar.
- Un ensemble de paquets RPM contenus dans ces fichiers tar.
En termes de relations, on va avoir :
- un produit va contenir plusieurs versions marketing,
- chaque version marketing va contenir plusieurs fichiers tar,
- chaque fichier tar va contenir plusieurs paquets (RPM dans ce cas de figure),
- un paquet peut faire partie de plusieurs fichiers tar.
Une fois qu'on a tout ça en place, le but est de remplir les données des versions avec Perl et DBIx::Class, puis de produire des pages HTML décrivant chaque release et ses différents composants.
Pour ne pas compliquer cet article, on va ignorer le problème de suivi des bogues et des corrections associés à chaque version.
2. La base de données
2.1 Structure des données
On a vu dans l'introduction que nous avons affaire à des données structurées avec des relations entre les différentes entités :
- relation « un avec plusieurs » (one-to-many) entre les produits, versions marketing et fichiers tar ;
- relation « plusieurs avec plusieurs » (many-to-many) entre les fichiers tar et les paquets.
On verra comment mettre en place ces relations dans la base de données.
3. Installation de tout le bazar
Sous Debian, faites :
sudo aptitude install mysql-server phpmyadmin
Notez qu'un serveur web va être installé par le jeu des dépendances.
aptitude va vous proposer d'installer une base de données dédiée à phpMyAdmin. Celle-ci servira à stocker des informations relatives aux structures de vos bases de données et aux fonctions avancées de phpMyAdmin. Accepter cette option est conseillé.
Vérifiez aussi que l'extension de sécurité suhosin (paquet php5-suhosin sous Debian) n'est pas installée. Celle-ci pose des problèmes au bon fonctionnement de phpMyAdmin.
Une fois qu'aptitude a fini son boulot, votre phpMyAdmin tout neuf est accessible avec votre butineur favori à l'adresse : http://locahost/phpmyadmin.
3.1 Création de la base de données et de ses utilisateurs
Une fois dans l'interface de phpMyAdmin, cliquez sur
pour créer la base de données « Integ ».Ensuite, il nous faut deux utilisateurs :
- integ_user, qui pourra lire les données et se connecter à la DB sans utiliser de mot de passe ;
- integ_mgr, qui pourra mettre à jour les données et devra utiliser un mot de passe pour se connecter.
Les utilisateurs sont ajoutés en deux étapes :
- Création avec l'onglet
et . Si nécessaire, des privilèges globaux (c'est-à-dire pour toutes les bases du serveur) peuvent être attribués.- Réglage fin des privilèges base par base. Ce réglage est accessible dans le tableau
dans l'onglet .Vous pouvez vérifier le résultat de la configuration des privilèges :
- Soit dans l'onglet
et le lien de la ligne . Dans le tableau , doit avoir juste un privilège « SELECT ».- Soit avec la commande :
$ mysql -u integ_user Integ
[ ... ]
mysql>
La création de l'utilisateur integ_mgr se fera avec une procédure similaire, mais en mettant un mot de passe et en attribuant les privilèges : « SELECT », « INSERT », « UPDATE » et « DELETE » pour la base « Integ ».
4. Création des tables
4.1 La table « Products »
Une fois le lien « Integ » cliqué dans le menu de gauche, phpMyAdmin va vous montrer la structure (vide) de la base « Integ » et vous inviter à créer votre première table, « Products » avec 3 colonnes :
- La colonne « id », qui va être la clef primaire de cette table. « id » est de type entier (« INT »), avec un index « PRIMARY », avec « AUTO INCREMENT » sélectionné ;
- La colonne « name », pour le nom marketing du produit. « name » est de type « VARCHAR », de 32 caractères de longueur, index « UNIQUE ». Pourquoi cet index ? Le marketing ne sortira jamais simultanément deux produits différents ayant le même nom. Cet index « UNIQUE » permet de faire respecter cette règle par la DB. Ça vous permet aussi de ne pas vérifier cette contrainte dans votre code (ou dans toute autre application qui voudrait exploiter cette base) ;
- La colonne « home_page » pour les informations internes sur le produit. « home_page » est de type « VARCHAR », de 256 caractères de longueur, et potentiellement « NULL » (des fois que le produit soit créé dans la base avant que sa home page ne soit disponible).
Derniers points importants :
- Le module DBIx::Class utilisé dans la 2e partie de cet article ne fonctionne pas si le nom de la colonne contient des espaces. C'est pourquoi un souligné («_») est utilisé.
- Le « Storage engine » doit être « InnoDB ». (On verra dans cet article pourquoi).
Enfin, n'oubliez pas de cliquer sur
en bas de page.4.2 Mais pourquoi 2 colonnes ?
Vu que le nom du produit doit être unique, on pourrait être tenté de s'en servir comme clef primaire et de se débarrasser de la colonne id. C'est une solution, certes fonctionnelle, mais qui présente un inconvénient majeur : le nom du produit devra aussi être utilisé dans les tables qui le référencent. Pour cet article, il devrait ainsi être utilisé dans la table qui contiendra les versions de produit. Si jamais le nom du produit change, il faudra retrouver et modifier toutes ces utilisations du nom du produit.
4.3 La table « ProductVersions »
Cette table a besoin d'une clef primaire « id », d'un champ « version » et d'un champ « date ». La création de ces colonnes est similaire aux colonnes de la table « Products ».
4.4 Relier un produit avec ses versions
On a maintenant deux jolies tables, mais elles ne sont pas reliées. Il faut créer la relation entre le produit (singulier) et ses versions (pluriel). Il s'agit d'une relation « 1 vers n ». En termes de base de données, cette relation est établie en créant dans la table « ProductVersions » une colonne « product_id » qui contiendra l'« id » du produit correspondant (« product_id » doit être de type « INT » avec Index à « INDEX »). Dans cette table, plusieurs versions pourront référencer le même « id », donc le même produit. Mais une version ne peut référencer qu'un seul produit. On a bien établi une relation « 1 vers n ».
Mais, va-t-on devoir coder de quoi vérifier la cohérence entre « product_id » et les « id » disponibles dans la table « Products » ?
4.5 Mieux relier le produit et ses versions : les clefs étrangères
J'ai pratiqué dans une vie antérieure le codage de cette vérification. Eh bien, ce n'est guère passionnant. Heureusement, les bases de données modernes fournissent maintenant un mécanisme assurant cette cohérence : les clefs étrangères (foreign keys, dans la langue de Shakespeare).
On peut déclarer à la DB que la valeur de « product_id » doit être présente dans la colonne « id » de la table « Products ». Ceci fait, la cohérence est assurée. Mais il y a encore mieux pour la maintenance des données par la suite.
Qu'advient-il si un produit est supprimé du catalogue ? Devra-t-on supprimer une par une les versions de la DB avant de pouvoir supprimer le produit ? Eh bien, non. MySQL offre aussi la possibilité de paramétrer le comportement en cas de destruction d'une ligne produit : soit interdire, soit passer à « NULL » (mettre le « product_id » à 0, rendant la version orpheline), soit propager (toutes les versions du produit sont détruites).
C'est cette dernière option qui nous intéresse.
Notez que les foreign keys sont disponibles avec le « Storage engine » « InnoDB », mais pas avec « MyISAM », qui est proposé par défaut. Si vous avez oublié ce détail, ce n'est pas grave, vous pouvez le changer en allant modifier ce paramètre dans l'onglet
de la table restée coincée en « MyISAM » (cherchez « Storage Engine »).Trêve de théorie, on peut ajouter cette relation dans la page « Structure » de la table « ProductVersions » avec le lien « Relation View » (si absent, voir le problème « InnoDB »). Si vous ne voyez pas de ligne « product_id », ajoutez un index sur cette colonne). Et là, mettez en place la « relation interne » (utilisé par phpMyAdmin) et la « Foreign key » (pour MySQL) avec l'option « ON DELETE: CASCADE ».
On peut aussi ajouter cette relation avec l'interface graphique « designer ». Cette interface est disponible en retournant dans la page de la base « Integ », onglet
, bouton .Vous devriez voir les deux tables non reliées. Cliquez sur le bouton
et suivez les instructions :- Sélectionnez la clef référencée : « id » dans la table « Products »,
- Sélectionnez la clef étrangère : « product_id » de la table « ProductVersions »,
- Choisissez « cascade » pour l'option « on delete ».
Et devant vos yeux émerveillés, voyez ceci apparaître :
(L'auteur admet ne pas aimer PHP, adorer phpMyAdmin et assumer ses contradictions...).
4.6 Blinder un peu plus
MySQL peut aussi assurer l'unicité de chaque couple produit-version. Il suffit de déclarer un index de type unique pour les deux colonnes « product_id » et « version ». Ça vous évitera de créer des lignes inutiles si votre code utilise au mauvais endroit une instruction de type « créer » au lieu du type « chercher ou créer ». (L'utilisation de ces fonctions sera expliquée plus en détails dans un article ultérieur).
4.7 La table « TarBalls »
Elle est créée de la même façon que la table « ProductVersions » avec une clef primaire « id », une « url » de type « varchar/128 », index « UNIQUE » et enfin, une clef étrangère « product_version_id » pointant sur l'id de la table « ProductVersions ».
4.8 La table « Packages » et la relation m-vers-n
Elle contient la colonne « id » et le nom du paquet (triplet nom-version.arch.rpm).
Mais là, ça se complique : un fichier tar peut contenir plusieurs paquets et chaque paquet peut faire partie de plusieurs fichiers tar. C'est une relation m-vers-n. Une seule colonne tarball_id dans la table « Packages » ne suffit pas.
La technique traditionnelle est d'utiliser une table intermédiaire pour relier tarball et paquet avec deux colonnes : une pour stocker l'id du tarball et une pour stocker l'id du paquet. Par exemple, si on crée un tarball (avec un id 42) contenant deux paquets (un déjà connu avec un id à 99 et un nouveau avec un id à 1024), on ajoutera dans la base :
- une ligne avec un id 42 dans la table « TarBalls »,
- une ligne avec un id 1024 pour le nouveau paquet dans la table « Packages »,
- deux lignes (42-99 et 42-1024) pour matérialiser les deux relations paquets-tarballs dans la table intermédiaire (nommée « TarballPackages »)
Cette table intermédiaire a :
- une colonne « tarball_id » de type entier, clef étrangère sur la colonne « id » de la table « TarBalls »,
- une colonne « package_id » de type entier, clef étrangère sur la colonne « id » de la table « Packages »,
- une clef primaire constituée des deux colonnes « tarball_id » et « package_id ».
Ce dernier point est important pour la suite avec le module Perl DBIx::Class::Schema::Loader. Ce module utilise des heuristiques assez contraignantes pour essayer d'interpréter correctement les relations entre les tables. Pour générer une relation « many-to-many » entre 2 classes DBIx::Class, la table de relation doit respecter ces règles.
Voici le schéma complet présenté par le « designer » de phpMyAdmin (les tables sont un peu maigres, mais c'est pour le bien de cet article) :
5. Suite au prochain épisode
Maintenant que la persistance des données est assurée par la base de donnée « Integ », il reste à voir dans un prochain article :
- Comment utiliser dbcidump et DBIx::Class::Schema::Loader pour mettre en place une interface entre un programme Perl et la base de données en écrivant un minimum de code ;
- Le module DBIx::Class pour effectuer des requêtes sur la base de données et exploiter les relations mises en place dans cet article.