Drupal 7 : Bases de données et requêtage

Avec le passage à la version 7, le CMS Drupal se dote de plusieurs fonctionnalités incorporées directement dans le core, et impose certains changements en matière de développement. Tous les utilisateurs – et plus généralement les développeurs – qui utilisent Drupal y sont forcément confrontés, aussi cet article propose de présenter les changements qui ont lieu au niveau des bases de données, de la technologie utilisée et de son impact sur la syntaxe des requêtes du client.

Cet article explique :

  • ce qu’apporte Drupal 7 au niveau de la couche base de données
  • l’équivalence syntaxique des requêtes SQL pour Drupal 6 et Drupal 7

Apparu dès fin 2010 en version bêta ("Drupal 7.0 beta 1 Released"), c’est seulement à partir de sa sortie officielle début 2011, que la communauté de développeurs s’approprie Drupal 7. Commence alors une période faste d’adaptation, durant laquelle les premiers utilisateurs tentent de l’expliquer au mieux dans blogs et articles en ligne afin de la rendre accessible au plus grand nombre. C'est ce foisonnement de blogs qui pousse les drupalistes à vouloir rendre la technologie toujours plus accessible, d'où les versions régulièrement mises à jour, et en ce qui nous concerne la version 7 de Drupal.

Parmi les nombreuses nouveautés proposées par Drupal 7, celles que nous allons observer ci-après sont celles qui concernent la couche base de données.

D’une part, au niveau de la technologie utilisée, Drupal 7 propose une nouvelle couche d’abstraction de base de données – PDO, pour PHP Data Object –, qui va permettre l’utilisation de plusieurs types de bases de données avec une syntaxe de requêtage commune. D’autre part, l’API prend dorénavant en charge la réplication maître/esclave, ce qui rend le système plus robuste et fiable en cas de panne d’un serveur, et améliore les performances en répartissant la charge de traitement des requêtes client.

Ce changement de technologie influe sur la syntaxe des requêtes, et offre la possibilité de réaliser des transactions, des insertions multiples ou retardées, et propose des query builders"constructeurs de requête" – liés aux instructions INSERT, UPDATE, DELETE, MERGE, et SELECT.

 

Evolutions liées à technologie utilisée

Qu’est-ce que PDO ?

La nouvelle couche d’abstraction PDO (l’acronyme pour PHP Data Objects) proposée par Drupal 7 permet aux développeurs d’écrire un code portable, compatible avec d’autres types de bases de données. Cette couche d’accès aux données utilise une API unifiée orientée objet, et va influencer la syntaxe des requêtes, un point que nous détaillerons dans la suite de l’article. Cette syntaxe va permettre d’utiliser plusieurs types de bases de données nativement avec le même code, notamment MySQL, MariaDB, PostgreSQL, SQLite. our utiliser Microsoft SQL Server ou Oracle, il est nécessaire de rajouter un module driver tiers, disponible sur le site officiel de Drupal.

Damien Tournoud, l’un des architectes de Drupal 7, qui a fondé l'association Drupal France et Francophonie en 2009, confirme cet aspect en affirmant que Drupal se comporte de la même façon quel que soit le type de base de données (http://www.journaldunet.com/developpeur/outils/damien-tournoud-drupal-7-quelles-nouveautes-quelle-strategie.shtml).

 

La réplication maître/esclave

Ce principe permet de rendre le système plus robuste, plus rapide et plus fiable : la configuration maître/esclave permet d’utiliser un esclave comme serveur de secours en cas de panne du serveur principal. La rapidité est accrue grâce à la répartition de la charge de traitement des requêtes du client entre maître et esclave, et permet ainsi de diminuer le temps de réponse. Certaines requêtes peuvent ainsi être envoyées à un esclave, et d’autres au maître. En effet, le site officiel de MySQL (http://dev.mysql.com) indique :

« Les requêtes SELECT peuvent être envoyées aux esclaves pour réduire la charge du maître. Les requêtes de modification des données sont envoyées au maître, qui les transmettra aux esclaves. Cette stratégie de répartition de charge est efficace si les lectures sont plus nombreuses que les écritures, ce qui est la situation la plus courante. »

Dans Drupal 7, la structure suivante renseignée dans le fichier "settings.php" permet de réaliser un dispositif de ce type de cette façon :

$databases['default']['default'] = array(
‘driver’ => ‘mysql’,
‘database’ => ‘drupaldb1′,
‘username’ => ‘username’,
‘password’ => ’secret’,
‘host’ => ‘dbserver1′,
);
$databases['default']['slave'][] = array(
‘driver’ => ‘mysql’,
‘database’ => ‘drupaldb2′,
‘username’ => ‘username’,
‘password’ => ’secret’,
‘host’ => ‘dbserver2‘,
);

On remarque que le tableau "database" possède un premier "default", correspondant à la clé de connexion, et un second, correspondant à la cible : "default", c’est-à-dire le maître, ou "slave" , l’esclave. La clé de connexion étant un identificateur unique pour une connexion à la base, forcément présente. Avec cette configuration, si le maître (par défaut) n’est pas disponible, le système va rechercher la base de données esclave. Il est possible de rajouter d’autres esclaves si besoin est, de la même façon qu’indiqué précédemment. Dans ce cas, les serveurs esclaves seront sélectionnés au hasard pour traiter les requêtes du client.

Plus qu’une configuration utile en cas de panne, cette nouveauté permet en outre de répartir la charge au niveau des traitements des requêtes du client. En effet, la syntaxe des requêtes permet d’exécuter une requête donnée sur la base de données esclaves, tout simplement en ajoutant dans les options passées à la requête :

$result = db_query(“SELECT nid, title FROM {node}”, array(), array(‘target’ => ’slave’,));

Le troisième paramètre passé à "db_query ()" correspond aux options qui indiquent comment la requête va se comporter. La couple clé/valeur passée en paramètre indique que la requête doit être traitée par un serveur esclave, s’il existe. (Plus d’infos ici : http://drupal.org/node/310072.)

 

Les transactions

Une transaction est une opération liée à une base de données et qui respecte certaines contraintes, regroupées sous l’acronyme ACID (Atomicité Cohérence Isolation Durabilité). C’est une opération qui garantit l’intégrité des données. Ainsi, lorsqu’une multitude de requêtes est envoyée au serveur de base de données simultanément, l’ensemble des requêtes est soit validé soit annulé en cas de problème. C’est une garantie très importante, dans le milieu bancaire notamment.

Ce système est géré par l’intermédiaire de la classe DatabaseTransaction, présente dans l’API de Drupal, qui fournit un support pour créer et gérer les transactions. Notons que certains drivers de bases de données ne supportent pas cette fonctionnalité, comme par exemple le moteur de stockage MySQL appelé MyISAM, contrairement à InnoDB qui, lui, gère les transactions.

Pour démarrer une nouvelle transaction, il faut procéder comme ceci :

$txn = db_transaction ();

Ainsi, la transaction restera ouverte aussi longtemps que la variable $txn existe.

Voici un exemple de transaction :

function my_transaction_function() {
$txn = db_transaction(); // La transaction s’ouvre ici
$id = db_insert('example')
->fields(array(
'champ1' => 'machaine',
'champ2' => 5,
))
->execute();
Mon_autre_fonction($id); // Appel d’une autre fonction
return $id;
// après le return $txn n’est plus disponible (elle peut également être détruite), la transaction est donc validée dans son intégralité
}

function my_other_function($id) {
// La transaction est toujours ouverte, car $txn existe
if ($id % 2 == 0) {
db_update('example')
->condition('id', $id)
->fields(array('champ2' => 10))
->execute();
}
}

 

Insertion multiple

Il peut s’avérer utile de pouvoir effectuer plusieurs insertions en base en utilisant une seule requête. Sur la plupart des bases de données, plusieurs déclarations seront exécutées ensemble à l’intérieur d’une autre transaction pour une intégrité des données optimale et une vitesse accrue. Avec MySQL, voici la syntaxe d’insertion multiple à utiliser :

$values = array(
array('title' => 'Example','uid' => 1,),
array('title' => 'Example 2','uid' => 1,),
array('title' => 'Example 3','uid' => 2,),
);
$query = db_insert('node')->fields(array('title', 'uid', 'created'));
foreach ($values as $record) {
$query->values($record);
}
$query->execute();

 

L’exemple ci-dessus va exécuter trois instructions d’insertion en base ensemble, en utilisant la méthode la plus efficace pour le driver de base de données actif. Cette syntaxe étant équivalente aux trois requêtes suivantes :

INSERT INTO {node} (title, uid) VALUES ('Example', 1);
INSERT INTO {node} (title, uid) VALUES ('Example2', 1);
INSERT INTO {node} (title, uid) VALUES ('Example3', 2);

 

Syntaxe de requêtage

Au niveau du requêtage, il est possible d’utiliser le même type de syntaxe qu’avec Drupal 6, mais la nouvelle version appelle à utiliser une syntaxe dynamique que l’on pourra ensuite utiliser quel que soit le type de base de données proposé par la couche d’abstraction PDO.

Voici un exemple d’équivalence de requête avec Drupal 6 et Drupal 7 (abrégés D6 et D7 dans la suite du texte) :

D6 (requête "statique") :

 $result = db_query("SELECT uid, name, status, created, access FROM {users} u WHERE uid <> 0 LIMIT 50 OFFSET 0");

D7 (requête "dynamique") :

$query = db_select('users', 'u');
$query
->condition('u.uid', 0, '<>')
->fields('u', array('uid', 'name', 'status', 'created', 'access'))
->range(0, 50);
$result = $query->execute();

 

Détaillons un peu les différents changements en matière de requêtage : Drupal 7 a choisi de proposer une API orientée objet, pour se comporter de façon uniforme quel que soit le type de base de données et donc définir une requête structurellement. Ainsi, les requêtes dynamiques sont construites dynamiquement par le CMS plutôt que fournies comme une chaîne de requête explicite en utilisant un "objet de requête" ("query object") (http://drupal.org/node/310075).

Par exemple, le "db_result" très usité qui permet de récupérer une seule valeur d’un champ lors d’une requête peut être remplacé par la méthode "fetchField()", comme ci-dessous :

D6 : 

$value = db_result(db_query('SELECT champ FROM {table}')); 

D7 :

 $value = db_query('SELECT champ FROM {table}')->fetchField(); 

 

Une fois la requête est construite, la méthode "execute()" permet de compiler et d’exécuter la requête :

$result = $query->execute();

 

Utilisation de marqueurs

Une autre différence entre les versions 6 et 7 de Drupal est la syntaxe des marqueurs présents dans les requêtes, qui doivent dorénavant commencer par un double point suivi d’une chaîne unique – qu’on appellera ici "label" :

D6 : 

SELECT nid, titre FROM {noeud} WHERE uid = $uid;

D7 : 

SELECT nid, titre FROM {noeud} WHERE uid =: uid;

":uid" est un espace réservé qui sera remplacé par une valeur littérale lors de l’exécution de la requête. Le tableau d’arguments de la requête doit être un tableau associatif, où les clés sont les labels utilisés et les valeurs, les valeurs correspondantes. Notons que le tableau peut être dans n’importe quel ordre.

Exemple :

 $table_alias = $query->join('user', 'u', 'n.uid = u.uid AND u.uid = :uid', array(':uid' => 5)); 

 

"Query builders"

Les instructions INSERT, UPDATE, DELETE doivent se comporter de façon uniforme, quel que soit le type de base de données. Par exemple, plutôt que :

INSERT INTO node (nid, title, body) VALUES (1, 'my title', 'my body');

il faudrait écrire :

$fields = array('nid' => 1, 'title' => 'my title', 'body' => 'my body');
db_insert('node')->fields($fields)->execute();

Les requêtes UPDATE et DELETE ont un fonctionnement similaire, et utilisent respectivement db_update() et db_delete().

 

Les conditions

Une méthode fournie par l’API de Drupal permet d’ajouter une ou plusieurs conditions à la requête, qui est décrite de la manière suivante :

$query->condition($field, $value = NULL, $operator = '=')

Voici un exemple très simple qui supprime la ligne de la table "node" qui a pour id 5 :

$num_deleted = db_delete('node')
->condition('nid', 5)
->execute();

 

D’autres méthodes sont disponibles, notamment db_query_range(), qui garantit une syntaxe commune, la syntaxe LIMIT étant différente selon le type de base de données utilisée.

Par exemple, pour retourner une liste des dix plus récents nœuds rédigée par un utilisateur donné, il est possible de réaliser ça classiquement en SQL :

SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid LIMIT 0, 10;

Mais il est préférable d’utiliser les fonctions de Drupal comme suit :

$result = db_query_range('SELECT n.nid, n.title, n.created FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid));
foreach ($result as $record) {
// Opérations diverses
}

 

Quelques remarques au sujet de la syntaxe des requêtes :

  • l’utilisation explicite d’un id utilisateur passé en paramètre à db_query() comme ci-dessus permet d’éviter les attaques par injection SQL, qui pourront être capturées et réduites à néant,

  • les accolades permettent d’ajouter un préfixe de base de données, elles doivent entourer toutes les tables concernées pour garantir une portabilité optimale, et ainsi coexister avec d’autres systèmes dans la même base de données et/ou schéma si nécessaire. Ces accolades appellent la fonction DatabaseConnection:: prefixTables(), qui renvoie la chaîne bien-préfixée.

 

Conclusion

Drupal 7 propose une API plus simple et complète et permet ainsi une meilleure portabilité du code. Par là, le fondateur du CMS Dries Buytaert veut toucher un public plus large et pouvoir installer le CMS sur davantage de sites importants, à fort trafic, qui utilisent d’autres technologies et notamment d’autres types de serveurs de bases de données. En jouant sur une compatibilité accrue, les drupalistes espèrent conquérir le marché du Web open source, et sont déjà très impliqués dans la version 8 de Drupal, dont la sortie n’est pourtant pas encore prévue mais que toute la communauté attend d’ici deux ou trois ans.

 

Quelques liens :

 

Terminologie

  • API (Application Programming Interface) : interface de programmation fournie par un programme, un logiciel, ou ici le CMS Drupal. Elle permet l’interaction des systèmes les uns avec les autres,

  • couche d’abstraction de base de données : interface entre le code d’une application du client et le serveur de base de données, orientée objet, permettant de traiter les échanges client/serveur sur plusieurs types de bases de données.