jemdev/dbrm

Database Relational mapping, Access library.


Keywords
database, data, library, validation, mapping, namespace
License
CECILL-2.1

Documentation

jemdev\dbrm : DataBase Relational Mapping


Installation

Avec Composer, ajouter ce qui suit dans la partie require de votre composer.json:

{
  "jemdev/dbrm": "dev-master"
}

Présentation et principe de fonctionnement.

Ce package permet un accÚs aux données d'une base de données relationnelle. L'idée fondatrice part du principe qu'on peut faire des lectures sur des tables multiples mais que l'écriture ne peut se faire que sur une seule table à la fois. Par conséquent, il devenait envisageable de créer des objets dynamiques pour chacune des tables sur lesquelles on souhaitait effectuer des opérations en écriture.

Des mĂ©thodes relativement simples permettent d'exĂ©cuter des requĂȘtes prĂ©parĂ©es pour la collecte de donnĂ©es. Pour l'Ă©criture, d'autres mĂ©thodes permettent de crĂ©er une instance pour initialiser une ligne d'une table donnĂ©e et d'affecter les valeurs souhaitĂ©es aux diffĂ©rentes colonnes de la table pour cette ligne. Selon que l'identifiant de la ligne est fourni ou non, l'Ă©criture sera une crĂ©ation ou une modification, voire une suppression.

Pour pouvoir créer ces instances dynamiques, un systÚme permet d'établir une sorte de cartographie du schéma de données, détaillant la liste des tables, des tables relationnelles et des vues qui sont présentes. Sur la base de ces informations, une instance pour une table donnée définit les propriétés en lisant la liste des colonnes, leur types et d'autres informations pratiques.

Lors de la connexion, si le fichier de configuration n'existe pas, il est automatiquement crĂ©Ă©. Par la suite, si on modifie la structure du schĂ©ma, mĂȘme si ce n'est que pour ajouter, modifier ou retirer une colonne dans une table, une mĂ©thode permet de rĂ©gĂ©nĂ©rer ce fichier de configuration. Il m'est apparu comme trĂšs peu pratique de devoir crĂ©er une classe pour chacune des tables, ces modifications de structure induisant la rĂ©-Ă©criture partielle de certaines de ces classes Ă  chaque fois. Ces classes sont donc gĂ©rĂ©es dynamiquement et sont, en rĂ©alitĂ©, des classes virtuelles.

Récupérer un objet de connexion

Configurer la connexion

Il est impĂ©ratif de crĂ©er un fichier contenant les paramĂštres de connexion au SGBDR. Ce fichier doit ĂȘtre nommĂ© dbCnxConf.php et ĂȘtre formatĂ© de la maniĂšre suivante :

<?php
/**
 * Fichier de configuration des paramÚtres de connexion à la base de données.
 * Ce fichier est généré automatiquement lors de la phase initiale d'installation.
 */
$db_app_server  = 'localhost';          // Adresse du serveur de base de données
$db_app_schema  = 'testjem';            // Schéma à cartographier (base de données de l'application)
$db_app_user    = 'testjem';            // Utilisateur de l'application pouvant se connecter au SGBDR
$db_app_mdp     = 'testjem';            // Mot-de-passe de l'utilisateur de l'application
$db_app_type    = 'pgsql';              // Type de SGBDR, MySQL, PostGreSQL, Oracle, etc..
$db_app_port    = '5432';               // Port sur lequel on peut se connecter au serveur
$db_meta_schema = 'INFORMATION_SCHEMA'; // SchĂ©ma oĂč pourront ĂȘtre collectĂ©es les informations sur le schĂ©ma de travail
/**
 * Création des constantes globales de l'application
 * NE PAS MODIFIER LES LIGNES SUIVANTES
 */
defined("DB_ROOT_SERVER")       || define("DB_ROOT_SERVER",         $db_app_server);
defined("DB_ROOT_USER")         || define("DB_ROOT_USER",           $db_app_user);
defined("DB_ROOT_MDP")          || define("DB_ROOT_MDP",            $db_app_mdp);
defined("DB_ROOT_SCHEMA")       || define("DB_ROOT_SCHEMA",         $db_meta_schema);
defined("DB_ROOT_TYPEDB")       || define("DB_ROOT_TYPEDB",         $db_app_type);
defined("DB_ROOT_DBPORT")       || define("DB_ROOT_DBPORT",         $db_app_port);
defined("DB_APP_SCHEMA")        || define("DB_APP_SCHEMA",          $db_app_schema);
defined("DB_APP_SERVER")        || define("DB_APP_SERVER",          $db_app_server);
defined("DB_APP_USER")          || define("DB_APP_USER",            $db_app_user);
defined("DB_APP_PASSWORD")      || define("DB_APP_PASSWORD",        $db_app_mdp);

Les types de SGBDR supportés

À ce jour, ce n'est utilisable qu'avec MySQL et PostGreSQL. Je n'ai pas testĂ© avec les forks de MySQL autres que MariaDb (percona et autres) mais dans la mesure oĂč ils sont compatibles, ça ne devrait pas prĂ©senter de blocage. La valeur Ă  utiliser pour la variable $db_app_type :

  • MySQL : mysql (fonctionne avec ce type pour MariaDB)
  • PostGreSQL : pgsql

Ce fichier devra ĂȘtre placĂ© dans le rĂ©pertoire oĂč sont situĂ©s vos Ă©ventuels autres fichiers de configuration selon l'architecture de votre application. Il conviendra par la suite de pouvoir fournir en temps voulu le chemin absolu vers ce fichier. DĂšs le dĂ©part, s'il n'existe pas, un autre fichier de configuration sera gĂ©nĂ©rĂ© automatiquement et sera indispensable au bon fonctionnement du package. Ce fichier sera gĂ©nĂ©rĂ© en deux version, le premier nommĂ© dbConf.php pourra ĂȘtre assez facilement lu par n'importe quel dĂ©veloppeur, le second qui sera privilĂ©giĂ© pour l'utilisation par l'application sera nommĂ© dbConf_compact.php et correspondra strictement au mĂȘme contenu mais compactĂ© et ramenĂ© sur une seule ligne. Ce fichier dĂ©crit en dĂ©tail l'ensemble de la structure de donnĂ©es, tables, tables relationnelles et vues, colonnes, clĂ©s et autres informations dĂ©taillĂ©es. Il sera utilisĂ© par les objets destinĂ©s Ă  toutes les opĂ©rations en Ă©criture, insertion, mises Ă  jour ou suppression.

MĂ©thodes globales accessibles

Deux méthodes de base seront indispensables :

  • setRequete($sql, $aParams = array(), $cache = true) DĂ©finit la requĂȘte Ă  exĂ©cuter, en option on peut indiquer des paramĂštres dans un tableau associatif oĂč chaque index est le nom de la colonne visĂ©e associĂ© Ă  sa valeur qui doit y ĂȘtre affectĂ©e, et un troisiĂšme paramĂštre permet de dĂ©sactiver la mise en cache du rĂ©sultat si ce cache est globalement activĂ© par dĂ©faut;
  • execute() Cette mĂ©thode permet d'exĂ©cuter directement une mĂ©thode dĂ©finie avec setRequete(). On peut alors envoyer une requĂȘte, un appel de procĂ©dure stockĂ©e ou une fonction utilisateur voire mĂȘme une requĂȘte en Ă©criture bien que cette derniĂšre option ne soit pas recommandĂ©e (Voir plus loin l'Ă©criture de donnĂ©es)

Deux autres méthodes nous intéressent principalement ici et ne seront utilisées que lorsqu'on devra enregistrer des création ou modifications de données :

  • startTransaction() DĂ©marre une transaction si les tables utilisent un moteur transactionnel. Toutes les requĂȘtes suivantes seront alors incluses dans une transaction jusqu'Ă  ce qu'on appelle la mĂ©thode finishTransaction();
  • finishTransaction($bOk) Termine une transaction : le paramĂštre attendu est un booleen, TRUE exĂ©cutera un COMMIT, FALSE exĂ©cutera un ROLLBACK;

Une autre méthode pourra se révéler pratique lors de la phase de développement de votre application :

  • getErreurs() Retourne la liste des erreurs rencontrĂ©es sous la forme d'un tableau

Lecture de données

Il n'y a pas de gĂ©nĂ©rateur de requĂȘtes, Ă  tout le moins pour l'instant. On devra Ă©crire soi-mĂȘme les requĂȘtes en lecture qui devront ĂȘtre exĂ©cutĂ©es pour la collecte de donnĂ©es.

Chaque requĂȘte peut ĂȘtre paramĂ©trĂ©e, sera exĂ©cutĂ©e avec PDO et retournera une donnĂ©e unique, une ligne de donnĂ©es ou bien un tableau de donnĂ©es voire mĂȘme un objet. On s'appuiera sur une instance de la classe jemdev\dbrm\vue qu'on dĂ©finira au prĂ©alable.

Exemple : par convention, l'instance de connexion sera la variable « $oVue » et aura été définie en amont (entendez le mot de « vue » au sens SQL du terme).

<?php
/* DĂ©finition de la requĂȘte */
$sql = "SELECT utl_id, utl_nom, utl_prenom, utl_dateinscription ".
       "FROM t_utilisateur_utl ".
       "WHERE utl_dateinscription > :p_utl_dateinscription ".
       "ORDER BY utl_nom, utl_prenom";
/* Initialisation de paramĂštre(s) */
$params = array(':p_utl_dateinscription' => '2015-10-15');
/* Initialisation de la requĂȘte */
$oVue->setRequete($sql, $params);
/* Récupération des données */
$infosUtilisateur = $oVue->fetchAssoc();

Note:

Le nom de l'objet $oVue dans cet exemple n'est pas anodin, il faut entendre le mot vue au sens SQL du terme. Une vue dans une base de donnĂ©es est une synthĂšse d'une requĂȘte d'interrogation de la base. On peut la voir comme une table virtuelle, dĂ©finie par une requĂȘte.

Les méthodes accessibles

Le nom des méthodes est inspiré de celles qu'on emploie avec l'extension MySQL. Le retours sont bien entendu similaires dans leur forme.

  • fetchAssoc() Retourne un tableau associatif de rĂ©sultats oĂč les index sont les noms des colonnes ou alias dĂ©terminĂ©s dans la requĂȘte;
  • fetchArray() Retourne un tableau oĂč chaque colonne est prĂ©sentĂ©e avec deux index, l'un numĂ©rique, l'autre associatif avec le nom de la colonne;
  • fetchObject() Retourne un objet oĂč chaque colonne est une propriĂ©tĂ©;
  • fetchLine($out = 'array') Retourne une seule ligne de donnĂ©es. On peut prĂ©ciser en paramĂštre quelle forme doit prendre le rĂ©sultat en passant une des constantes suivantes :
    • vue::JEMDB_FETCH_OBJECT = 'object' : indiquera un retour sous forme d'un objet;
    • vue::JEMDB_FETCH_ARRAY = 'array' : indiquera un retour sous forme d'un tableau avec double index numĂ©rique et associatif;
    • vue::JEMDB_FETCH_ASSOC = 'assoc' : indiquera un retour sous forme d'un tableau associatif;
    • vue::JEMDB_FETCH_NUM = 'num' : indiquera un retour sous forme d'un tableau indexĂ© numĂ©riquement;
  • fetchOne() Retourne une donnĂ©e unique;

Écriture de donnĂ©es

Les méthodes accessibles

Sur une instance donnée, vous disposez des méthodes suivantes :

  • init($aPk = null) : Initialise l'instance de la ligne. En arriĂšre plan, l'objet sera construit dynamiquement avec comme propriĂ©tĂ©s les colonnes de la table visĂ©e;
  • sauvegarder() : Enregistre les modifications apportĂ©es aux propriĂ©tĂ©s par une requĂȘte INSERT ou UPDATE selon qu'on a dĂ©terminĂ© ou non la clĂ© primaire;
  • supprimer() : Supprime la ligne de la table par une requĂȘte DELETE. Dans le cas oĂč un moteur transactionnel serait utilisĂ© et que des clauses d'intĂ©gritĂ© rĂ©fĂ©rentielles auraient Ă©tĂ© dĂ©finies (CONSTRAINT), la mĂ©thode pourra retourner une erreur si des donnĂ©es faisant rĂ©fĂ©rences Ă  la ligne devant ĂȘtre supprimĂ©e existent encore dans les tables liĂ©es;
  • startTransaction() : DĂ©marre une transaction SQL;
  • finishTransaction($bOk) : Termine une transaction SQL, le paramĂštre attendu est un boolĂ©en indiquant si on doit effectuer un COMMIT ou un ROLLBACK;

Mise en pratique

On écrit des données, comme mentionné en introduction, que sur une seule table à la fois. Pour ce faire, on crée un objet représentant une ligne de ladite table. Voici d'abord un exemple schématique :

<?php
/* On crée l'instance de la ligne à partir du nom de la table cible */
$oInterlocuteur = $oDbrm->getLigneInstance('t_interlocuteur_int');
/*
 * On détermine si on dispose ou non de la clé primaire de la ligne
 * et on stocke ça dans un tableau associatif
 */
$aPk = (!empty($int_id)) ? array('int_id' => $int_id) : null;
/* On initialise l'instance */
$oInterlocuteur->init($aPk);
/*
 * DÚs cet instant, notre objet présente chaque colonne de la
 * table t_interlocuteur_int comme des propriétés qu'on peut modifier
 */
$oInterlocuteur->int_nom    = $int_nom;
$oInterlocuteur->int_prenom = $int_prenom;
if(!is_null($int_dateinscription))
{
    $oInterlocuteur->int_dateinscription = $int_dateinscription;
}
/* On peut maintenant sauvegarder ces informations */
$enreg = $oInterlocuteur->sauvegarder();
/*
 * Terminé, les écritures pour cette ligne sont terminées.
 * On peut récupérer la valeur de la clé primaire si nécessaire et s'il
 * s'agissait d'une création. Cette clé primaire est automatiquement gérée
 * et initialisée dans l'instance.
 * S'il y a eu une erreur, la méthode sauvegarder retournera l'erreur, sinon
 * elle retournera TRUE
 */
if(true == $enreg)
{
    $int_id = $oInterlocuteur->int_id;
    /*
     * Ici, si par exemple vous avez d'autres données à enregistrer, données qui
     * dépendent la la réussite de ce premier enregistrement, vous continuez
     * sur l'enregistrement suivant, exemple :
     */
    $oAdresse = $oDbrm->getLigneInstance('t_adresse_adr');
    $aPk = (!empty($adr_id)) ? array('adr_id' => $adr_id) ? null;
    $oAdresse->init($aPk);
    $oAdresse->int_id = $int_id;
    $oAdresse->adr_numero = $adr_numero;
    // ... etc...
    $enreg = $oAdresse->sauvegarder();
    // etc... suite selon les besoins.
}
else
{
    // Ici, le code permettant la gestion de l'erreur selon vos propres maniĂšres de faire.
}

Une utilisation pratique vous amĂšnera sans doute Ă  rĂ©partir les requĂȘtes en Ă©criture sur diffĂ©rentes tables dans diffĂ©rentes fonctions/mĂ©thodes appelĂ©es Ă  partir d'un endroit unique, ce qui vous permettra d'utiliser au besoin le mode transactionnel. En dĂ©marrant la transaction au dĂ©part, vous exĂ©cutez chaque enregistrement, et si une des mĂ©thodes retourne FALSE Ă  cause d'une erreur, vous pourrez interrompre la succession des enregistrements et terminer la transaction par un ROLLBACK, Ă©vitant ainsi de pourrir vos tables avec des donnĂ©es orphelines ou incohĂ©rentes.

Ce qu'on ne peut pas faire (pour l'instant)

Actuellement, il reste quelques éléments en TODO-LIST et en particulier, lors de l'écriture de données, la possibilité d'affecter non pas une valeur mais un appel de fonction SQL. Supposons par exemple que vous vouliez utiliser une fonction de chiffrement intégrée de MySQL pour affecter une valeur. Il n'est pour l'instant pas possible de faire ceci :

$instanceLigne->nom_colonne = "AES_ENCRYPT('valeur', 'Clé de chiffrement')";

Comment contourner le problĂšme.

Pour une utilisation quotidienne, ce n'est pas un réel problÚme, ce type de cas particulier étant relativement rare. Si cependant vous devez pouvoir effectuer une telle opération, vous avez deux options.

  • La premiĂšre consiste Ă  envoyer une valeur en clair et ajouter un trigger sur la table avec un BEFORE INSERT qui exĂ©cutera alors la fonction SQL Ă  appliquer sur la valeur pour l'affecter Ă  la colonne;
  • La seconde consiste Ă  Ă©crire vous-mĂȘme la requĂȘte en Ă©criture INSERT ou UPDATE et Ă  la faire exĂ©cuter avec la mĂ©thode execute() de l amaniĂšre suivante :
<?php
/* On a d'abord besoin d'une instance de jemdev\dbrm\vue */
$oVue = $oDb->getDbrmInstance();
/* On dĂ©finit la requĂȘte SQL d'insertion */
$sql  = "INSERT INTO matable (col_login, col_motdepasse)".  
        "VALUES('Toto', AES_ENCRYPT('valeur', 'Clé de chiffrement'))";  
$oVue->setRequete($sql);  
/* ExĂ©cution de la requĂȘte. */
$enreg = $oVue->execute();

La suite du code ne change pas.

Il n'a pas Ă©tĂ© prĂ©vu pour l'instant de pouvoir effectuer une mise Ă  jour ou encore une suppression de lignes multiples dans la mesure oĂč une mise Ă  jour s'effectuera uniquement en fonction de la valeur d'une clĂ© primaire. Pratiquant l'utilisation au quotidien de ce package depuis dĂ©jĂ  de nombreuses annĂ©es et ce sur une application de gestion, je n'ai en rĂ©alitĂ© jamais eu besoin d'implĂ©menter cette possibilitĂ©. Et pour les rares fois oĂč ça doit se produire, je peux contourner ce manque en collectant la liste des clĂ© primaires Ă  prendre en compte dans une mise Ă  jour et chaque ligne sera traitĂ©e individuellement dans une boucle.

Temps d'exĂ©cution des requĂȘtes

Ce petit systĂšme s'appuie sur la mĂ©thode native error_log de PHP pour la journalisation de requĂȘtes lentes. Au fil du dĂ©veloppement d'une application, il peut ĂȘtre utile d'identifier des goulets d'Ă©tranglement qui ralentissent l'application. On peut alors simplement activer un systĂšme de mesure qui effectuera un chronomĂ©trage systĂ©matique de toutes les requĂȘtes. On paramĂštre le systĂšme en lui indiquant :

  • Le type de journalisation : « php », « fichier » ou « courriel »;
  • La durĂ©e minimale en secondes Ă  partir de laquelle on journalise une requĂȘte;
  • En option le chemin absolu vers un fichier journal oĂč seront enregistrĂ©es les informations si le mode « fichier » a Ă©tĂ© dĂ©fini;
  • En option une adresse de courriel oĂč adresser les messages d'avertissement si le mode « courriel » a Ă©tĂ© dĂ©fini.

La mise en oeuvre est trĂšs simple, exemple :

$mode = 'fichier';
$maxtime = 1;
$fichier = 'app/tmp/logs/journaldb.log';
$this->_oDb->activerModeDebug($mode, $maxtime, $fichier);

C'est tout : une fois ceci lancĂ©, il suffit alors de naviguer normalement dans l'application, spĂ©cialement dans les parties qui affichent des ralentissements apparents, puis de vĂ©rifier le fichier journal pour y trouver Ă©ventuellement des requĂȘtes qui devraient alors ĂȘtre optimisĂ©es pour une accĂ©lĂ©ration.

Pour le mode « fichier », si le fichier n'existe pas, il sera créé.

Une gestion de cache dynamique

Un problĂšme d'accĂšs Ă  la configuration du serveur MySQL sur lequel je travaillais m'interdisait de paramĂ©trer le cache intĂ©grĂ© et mĂȘme tout simplement de l'activer par dĂ©faut. Souhaitant pouvoir disposer d'un systĂšme de gestion de cache de requĂȘtes, j'ai ajoutĂ© des classes permettant de gĂ©rer cet aspect.

Globalement, chaque requĂȘte en lecture peut, si le cache est activĂ©, stocker le rĂ©sultat en cache sur fichier voire mĂȘme sur MemCache. Toute Ă©criture sur une des table va rĂ©gĂ©nĂ©rer le cache pour les requĂȘtes oĂč est impliquĂ©e la table en question. La durĂ©e de vie du cache est donc fonction de l'exĂ©cution de nouvelles Ă©critures et non d'une durĂ©e de vie prĂ©-dĂ©finie. Si le rĂ©sultat d'une requĂȘte est valide pendant trois minutes et qu'une Ă©criture intervient, le cache est renouvelĂ©, si ce mĂȘme rĂ©sultat est toujours valide aprĂšs trois semaines, il est parfaitement inutile de le rĂ©gĂ©nĂ©rer.

Certaines mĂ©thodes permettent de rĂ©gĂ©nĂ©rer manuellement le cache pour certaines tables. Par exemple, si lors d'une Ă©criture sur une table un trigger va dĂ©clencher l'exĂ©cution d'une procĂ©dure stockĂ©e crĂ©ant des Ă©critures sur d'autres tables, il sera important de rĂ©gĂ©nĂ©rer le cache sur ces autres tables. Il n'est pas possible de dĂ©tecter ces Ă©critures en PHP dans la mesure oĂč c'est le SGBDR qui gĂšre ça directement. De mĂȘme si des tĂąches CRON dĂ©clenchent des Ă©critures sans passer par PHP, il n'est pas possible d'intercepter cette information pour mettre Ă  jour le cache correspondant, il conviendra donc d'Ă©crire un code PHP qui effectuera ce nettoyage, code qui devra ĂȘtre exĂ©cutĂ© dans une tĂąche Ă  ajouter au CronTab.

Par défaut, le cache n'est pas activé, et si vous avez la possibilité de gérer le cache intégré de votre SGBDR, ce sera alors une solution préférable et plus performante.


Conclusion

Ce package se veut simple d'utilisation de façon à ne pas perdre le développeur dans les complications de l'implémentation, et ce sans avoir à se préoccuper du type de serveur de base de données utilisé, que ce soit MySQL, Oracle, SQL-Server ou tout autre serveur.

À venir

Il reste cependant Ă  dĂ©velopper le code qui permettra d'utiliser des SGBDR autres que MySQL ou PostGreSQL, codes qui pour l'instant n'existent pas. Il s'agit de pouvoir construire le tableau de configuration d'un schĂ©ma de donnĂ©es. MySQL et PostGreSQL implĂ©mentent INFORMATION_SCHEMA, ce qui facilite grandement ce travail, mais tous les SGBDR ne l'implĂ©mentent pas, comme par exemple Oracle. Il existe cependant d'autres maniĂšre de collecter ces informations pour aboutir au mĂȘme rĂ©sultat.

Par la suite, le fonctionnement s'appuyant sur PDO, l'intégration de jemdev\dbrm pourra se faire dans n'importe quel projet.

Les projets Ă  plus long terme

L'idĂ©e d'un gĂ©nĂ©rateur de requĂȘtes automatisĂ© flotte dans l'air depuis pas mal de temps mais requiert un niveau de connaissances en mathĂ©matiques que je n'ai malheureusement pas. Il est question de s'appuyer sur la thĂ©orie des graphes pour dĂ©terminer quelles jointures devront ĂȘtre Ă©tablies pour n'avoir Ă  dĂ©finir que les colonnes de telle ou telle table est attendue pour que le moteur construise automatiquement le chemin appropriĂ©. Le fichier de configuration permet d'ores et dĂ©jĂ  de crĂ©er une matrice (le code n'est pas intĂ©grĂ© dans le package mais est dĂ©jĂ  prĂȘt et opĂ©rationnel), il reste Ă  dĂ©finir l'algorithme appropriĂ© de façon Ă  construire des requĂȘtes respectant les standards les plus exigeants.

Toute contribution en la matiĂšre sera bienvenue.