Gestion des schémas dans un module Drupal

Sans grosses trompettes, ni fanfares, avec Drupal 6 est apparue Schema API, une batterie de fonctions dédiées à la gestion des schémas en base de données qui change réellement la vie.

Cet article a besoin d'un bon lifting, c'est pour cela que les sources ne sont pas fournis avec

Constat initial

Dans le précédent épisode, nous avions construit un module prenant en charge une petite table node_produit nous permettant de décrire un produit de notre liste de courses.

De ce travail nous pouvons tirer quelques conclusions :

  • La rédaction de courses.install et plus précisément de hook_install et hook_uninstall est assez fastidieuse, nous obligeant à prendre en compte deux bases de données avec chacune leur exotisme syntaxique.
  • L'insertion d'un enregistrement et sa mise à jour sont elles aussi assez pénible, impliquant l'écriture de longues requêtes insert et update, avec le risque d'oublier un champ ou de se planter dans le type.

C'est précisément sur ces deux aspects que Schema API va nous apporter un bol d'air.

Maintenant, si vous n'êtes pas encore familiarisé avec les hooks, je vous conseille de relire le tutoriel, Créer son premier module Drupal. A partir de là, tout ce qui suit est basé sur le module décrit dans le tutoriel Créer un module "type de contenu".

Définition d'un schéma

Avec Drupal 6, il nous est donc maintenant possible de définir un schéma de base de donnée par module comprenant une ou plusieurs table de manière très simplifiée. Comme toujours, l'histoire commence avec l'implémentation d'un nouveau hook, hook_schema.

Dans notre précédente version de la liste de courses, la création de la table associée au module, node_produit était prise en charge par le fichier courses.install. C'est dans ce même fichier que va se retrouver notre nouveau hook_schema qui prendra la forme suivante :

/**
* Implementation of hook_schema().
*/

function courses_schema() {
$schema['node_produit'] = array(
// Description de la table
'description' => t('Le node Produit.'),

// Liste des champs
'fields' => array(
'nid' => array(
// Description du champ
'description' => t('La clef primaire de notre node.'),
// type du champ
'type' => 'int',
// Dans le cas d'un entier, champ signé ou non
'unsigned' => TRUE,
// Le champ a t-il le droit d'être vide
'not null' => TRUE),
'quantite' => array(
'description' => t('La quantité de produit.'),
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1),
'seuil' => array(
'description' => t('Le seul minimum avant rachat.'),
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 2)
),

// Clef primaire
'primary key' => array('nid')
);
return $schema;  
}

Si vous oubliez de faire le return à la fin de votre fonction, cela va désactiver tous les autres schemas, y compris ceux des modules core.... La raison en est une anomalie au niveau de la reconstruction du cache qui entraîne le vidage complet de celui-ci.

Cette fonction renvoie donc un tableau contenant une liste de tables. Ici nous n'en avons qu'une seule, node_produit.

Chaque élément du tableau est un tableau associatif définissant les caractéristiques de la table. Nous avons donc description pour le commentaire, fields pour la liste des champs, et enfin primary key pour la définition de la clef primaire. A noter que cette définition accepte un second argument entier optionnel précisant le nombre d'octets ou de caractères à prendre en compte.

Chaque champ est encore une fois un tableau associatif définissant une description, une valeur par défaut, le fait que le champs puisse être vide ou pas, le fait que s'il s'agit d'un entier, si celui-ci est signé ou pas, et enfin le type du champ. La liste des types disponibles et leur équivalence par base de données est indiquée ici. Nous voyons d'ailleurs qu'il est aussi possible d'être encore plus précis en définissant une taille (propriété size) pour chacun des types. Il est de même possible de définir la taille maximum des chaînes avec length et la précision des flottants (numeric) avec precision et scale.

Nous concernant, il s'agit d'une version encore assez simple d'une définition de table. L'API permet en réalité d'aller encore plus loin en définissant des indexes et des clefs uniques. Dans un cas comme dans l'autre ces définitions prennent la forme array(nom => spécification). Nom étant le nom de la définition (de l'index ou de la clef unique) et spécification un tableau simple contenant la liste des champs composant la définition. Ce qui nous donne par exemple (à ne pas inclure dans le fichier courses.install) :

function courses_schema() {
$schema['node_produit'] = array(
// Description de la table
'description' => t('Le node Produit.'),
'fields' => array( ... ),
'primary key' => array ( ... ),

// Définition des indexes
'indexes' => array(
'index1'        => array('champ1'),
'index2'        => array('champ2'),
...
),

// Définition des clefs uniques
'unique keys' => array(
'unique1' => array('champ3', 'champ4'),
...
)
}
...
}

Il existe un module Schema qui une fois installé et activé, vous permet par l'onglet Inspect, d'obtenir les définitions de schémas de toutes les tables présentes en base de données. C'est très pratique pour mettre à jour un vieux module et lui permettre rapidement d'exploiter Schema API.

Création et suppression du schéma

Une fois notre schéma définit, il nous faut maintenant modifier hook_install et hook_uninstall, pour l'exploiter.

/**
* Implementation of hook_install().
*/

function courses_install() {
$result=drupal_install_schema('courses');
return $result;
}

/**
* Implementation of hook_uninstall().
*/

function courses_uninstall() {
$result=drupal_uninstall_schema('courses');
return $result;
}

Comme vous le voyez, difficile de faire plus simple et surtout plus portable. Car ces fonctions vont générer toute seules le code SQL adapté à la base de donnée cible. Rien que cela, ça change la vie. Mais ce n'est pas fini, loin de là.

Mise à jour du schéma

Un aspect non abordé lors du précédent tutoriel est celui de la mise à jour d'un schéma de base de donnée associé à l'évolution d'un module. En effet, un module va vivre dans le temps et sûrement vous obliger à ajouter, enlever ou modifier des champs ou des tables. Sous Drupal, les mises à jours sont gérées par une URL spéciale http://mon.site.drupal.fr/update.php. Lorsque cette adresse est utilisée, Drupal affiche une liste des modules avec, pour certains, un numéro de version indiquant qu'une mise à jour est en attente d'application.

Cette page exploite un hook qui est lui aussi spécifique au fichier .install : hook_update_X. Pour bien comprendre, imaginons que nous ayons à ajouter une nouvelle colonne dans notre table node_produit. Ce champ permettrait de savoir si le produit est important ou pas. Pour automatiser cette mise à jour, nous aurions du effectuer deux modifications dans le code de courses.install. Tout d'abord modifier les deux versions de notre code SQL de création de table dans course_install, et ensuite ajouter un code comme celui-ci :

function courses_update_1() {
$ret = array();
$ret[] = update_sql("ALTER TABLE {node_produit} ADD COLUMN important tinyint(1) NOT NULL DEFAULT '0'");
return $ret;
}

Et encore, il faudrait même faire mieux que cela pour que cela fonctionne sur toutes les bases de données mais pour l'exemple, ça suffira.

La première remarque que l'on se fait est "pourquoi faire cela deux fois" ? Simplement parce que Drupal lors d'une installation ne lance pas de mise à jour. Pour lui, une installation consiste à exécuter hook_install qui contient obligatoirement la version à la plus à jour du schéma.

Lorsqu'il installe un module, il va donc écrire en base de donnée que le schéma de ce module est installé en version X, X étant l'indice de hook_update_X le plus élevé qu'il aura trouvé. Ce n'est donc que plus tard, une fois les tables crés et le module installé, que s'il trouve lors de l'exécution de update.php un hook_update_Y avec Y > X, qu'il va chercher à le lancer. Une fois que c'est fait, il va stoquer Y comme version de schéma pour le module, jusqu'à la prochaine mise à jour.

Avec l'API Schema, ce principe ne change pas, la seule chose qui évolue, et qui est la raison pour laquelle je ne me suis pas étendu sur la faiblesse multi-base de mon précédent code, est l'ajout d'une grosse batterie de fonctions permettant de modifier le schéma dynamiquement et de générer un code SQL compatible avec n'importe quelle base prise en charge. Ainsi mon courses_update_1 devient :

/**
* Implémentation de hook_update_X
*/

function courses_update_1() {
$ret = array();
db_add_field($ret, 'node_produit', 'important', array(
'description'   => 'Ce produit est important !!',
'type'       => 'int',
'unsigned'     => TRUE,
'not null'     => TRUE,
'default'    => 0)
);
return $ret;
}

C'est sur, c'est un peu plus verbeux mais beaucoup plus lisible et totalement portable d'une base à l'autre. La fin des mises à jour qui partent en torche pour des erreurs de syntaxe... Mais comme je le disais plus haut, Schema API ne change pas le fonctionnement de Drupal. Il va donc aussi falloir aussi modifier le code de courses_schema pour ajouter à peu prés la même chose :

function courses_schema() {

...

'fields' => array(

...

'seuil' => array(
'description' => t('Le seul minimum avant rachat.'),
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 2),

// Ajout à la version 1 du schéma
'important' => array(
'description'   => 'Ce produit est important !!',
'type'       => 'int',
'unsigned'     => TRUE,
'not null'     => TRUE,
'default'    => 0)
),

// Clef primaire
'primary key' => array('nid')
);
return $schema;  
}

Voilà, notre fichier courses.install est maintenant complet. Voyons maintenant comment tout cela va nous simplifier la vie pour le reste du code.

Exploitation du schéma

Le truc pénible avec la création d'un node de type contenu est typiquement d'écrire, et de maintenir au fil des versions, hook_insert et hook_update. Avec Schema API, la donne change et apparaît une fonction "magique" : drupal_write_record. Son rôle est d'écrire directement, sans une ligne de SQL, un objet complet dans une table du schéma. Voyons ce que cela donne :

function courses_insert($node) {
drupal_write_record("node_produit", $node);
}

function courses_update($node) {
drupal_write_record("node_produit", $node, 'nid');
}

Avouez que c'est mieux !! La seule différence entre la fonction insert et update, est que dans le second cas, nous spécifions, en plus du nom de la table et de l'objet source de donnée, un troisième paramètre indiquant dans quel champ de l'objet se trouve la clef primaire. C'est ce troisième paramètre qui fait donc la différence entre mise à jour et insertion.

Grâce à cette simple fonction, notre module devient éminemment plus maintenable qu'avant. Ajouter ou Enlever un champ dans une table ne demande plus aucune intervention dans le code.

En revanche, aucune fonction magique pour hook_delete et hook_load qui continue à utiliser une syntaxe SQL déjà très simple.

Conclusion

Je pense que nous seront d'accord sur le fait que Schema API est une réelle avancée qui mériterait plus de lumière. Elle simplifie énormément le code, le rend plus maintenable et facile à debugger, mais aussi permet de rendre plus abstraite encore la base de donnée utilisée. Et tout cela, sans être une usine à gaz, ce qui déjà en soit est appréciable.

Publier un nouveau commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement.
  • Les adresses de pages web et de courriels sont transformées en liens automatiquement.
  • To highlight piece of code, just surround them with <code type="language"> Your code &tl;/code>>. Language can be java,c++,bash,etc... Everything Geshi support.
  • Les lignes et les paragraphes vont à la ligne automatiquement.

Plus d'informations sur les options de formatage

CAPTCHA
Cette question est là pour déterminer si vous êtes humain ou pas...