Débugger une requête dynamique

Avec Drupal 7 a été introduit une toute nouvelle couche d'accès aux bases de données. Et parmi les nombreuses possibilités offertes par cette Database API, nous trouvons la manipulation dynamiquement des requêtes SQL pour ajouter une jointure, un tri, une condition, etc.

Mais cette fantastique possibilité induit aussi de véritables maux de crâne lorsqu'arrive l'inévitable heure du debuggage.

Requêtes dynamiques avec Drupal 6

Avec Drupal 6, lorsque l'on devait créer une requête à géométrie variable, cela donnait un code de ce genre :

if (!is_null($uid)) {
  $bout = "n.uid=$uid"; // oui je sais, ce n'est pas safe, c'est pour l'exemple ;-)
} else {
  $bout = '';
}

$query = "
  SELECT n.nid, n.title
  WHERE
    $bout
    n.type='mon_type' AND
    n.status=1
  ORDER BY n.created DESC"
;

$result = db_query($query);
while ($row = db_fetch_object($result)) {
  ...
}

Alors pas de doute, c'est moche. Cela peut être rendu moins moche avec les placeholders, mais bon, pas terrible tout de même...

Drupal 7 et db_select

Maintenant, écrivons le même code en utilisant les requêtes dynamiques de Drupal 7 :

// Création d'une requête en définissant le "from"
$query = db_select('node', 'n');

// Ici une clause "where" conditionnée par la présence d'un user ID valide
if (!is_null($uid)) {
  $query->condition('n.uid', $uid);
}

// Clause Where : n.type='mon_type'
$query->condition('n.type', 'mon_type');

// Clause Where : n.status=1
$query->condition('n.status', 1);

// On trie le tout
$query->orderBy('n.created', 'DESC');

// Et on itère sur le résultat, tout simplement...
foreach ($query->execute() as $row) {
  ...
}

Comme vous pouvez le constater, cela n'a pas la même gueule... Le sujet de ce papier n'étant pas d'expliquer en détail le fonctionnement de cette API, je vous conseille d'aller bouquiner l'excellent documentation disponible ici.

Et quant ça coince ?

Seul souci dans cette histoire, comment debugger une requête qui plante. Et justement, la requête donnée plus haut plante salement (c'est même fait exprès ;-). Avant, avec Drupal 6, un simple echo $query aurait suffit. Et avec Drupal 7 ce n'est guère plus compliqué. $$echo (string)$query; SELECT FROM {node} n WHERE (n.type = :db_condition_placeholder_0) AND (n.status = :db_condition_placeholder_1) ORDER BY n.created DESC

En effet, $query est maintenant un objet dont la classe a le bon goût d'implémenter la méthode magique __toString() et de fournir en résultat une requête SQL correctement formée, ou presque. Presque car nous avons encore ces "placeholder" qui nous bouchent la vue. Qu'à cela ne tienne, il suffit d'aller piocher les valeurs de ces placeholders, qui sont les paramètres de notre requêtes, grâce à la méthode $query->getArguments() qui nous renvoie un tableau utilisable par la fonction PHP strtr : $$echo strtr((string)$query, $query->getArguments()); SELECT FROM {node} n WHERE (n.type = mon_type) AND (n.status = 1) ORDER BY n.created DESC

Voilà qui est bien mieux. On voit immédiatement qu'il manque quelque chose d'assez fondamental entre le SELECT et le FROM, les champs à remonter. Cela se corrige facilement :

// Création d'une requête en définissant le "from"
$query = db_select('node', 'n');

// Ajout des champs n.nid et n.title
$query->fields('n', array('nid', 'title'));

Voilà, c'est réglé. Et nous pouvons donc ajouter à nos fonctions pratiques celle qui permet de debugger une requête dynamique :

function db_debug($query) {
  return strtr((string)$query, $query->getArguments()+array('{'=>'', '}'=>''));
}

Cette fonction n'est rien de plus qu'une formalisation de la technique énoncée plus haut, à ceci près que je vire au passage les accolades permettant de tester la requête facilement dans une console mysql ou postgresql.

Happy hunting :)

SebCorbin (non vérifié), le ven, 23/03/2012 - 14:33

Ou avec Devel, il y a la fonction dpq()

http://drupalcode.org/project/devel.git/blob/refs/heads/7.x-1.x:/devel.m...

(Tu change souvent de design ces temps-ci !)

Yoran, le ven, 23/03/2012 - 16:17

Là je ne peux pas dire, je n'ai jamais utilisé devel. Faudra que je le teste un jour.

Pour le design, il a pas changé depuis un certain temps. Tu ne confonds pas avec celui d'artisan ?

JulienD (non vérifié), le dim, 01/04/2012 - 00:53

Pas mieux que Seb avec la fonction dpq() : http://api.drupal.org/api/devel/devel.module/function/dpq/7
Devel apporte quand même plusieurs choses utiles pour développer/déboguer tu devrais vraiment y jeter un coup d’œil :)

Yoran, le dim, 01/04/2012 - 11:48

Mais j'y ai jeté un coup d'oeil entre temps et j'en ai déjà retiré deux trois choses utiles pour mes propres outils de debuggage (notamment le fonctionnement du logger de requêtes de D7, bien pratique).

Ceci étant dit, je ne nie pas l'intérêt des modules de service. Mais l'objectif de ce site, s'il est est un, c'est d'apprendre à faire, et non d'apprendre à utiliser. Dit autrement savoir que dpq permet de faire ce que cet article décrit c'est bien, mais si l'on se borne à cela, on est dés-facto cantonné à un cadre d'usage restrictif. En revanche savoir comment dpq fonctionne en interne permet d'aller plus loin, soit pour coder un usage que dqp n'autorise pas (par exemple moi je ne logue jamais sur le navigateur, mais dans une console), soit en permettant d'exploiter cette technique dans un autre cadre que celui de dpq (par exemple un outil de monitoring).

Et puis de tout façon, il y a sur le web suffisamment, je pense, de sites dédiés à l'explication de l'usage du module X ou Y. Je ne verrais pas l'intérêt d'en faire un de plus ;-)

nyl (non vérifié), le mer, 09/05/2012 - 11:46

En passant, dpq() a le désagréable défaut de passer par dpm(); qui passe donc par les sessions Drupal et leur affichage dans le template page, c'est donc le mal pour débogguer. Par exempmle si on met un exit avant l'affichage du template; ça n'affiche plus rien :D

Y'a rien qui empeche de se faire sa propre version de dpq() avec un kpr() à la place d'un dmp() mais la fonction de yoran fait parfaitement le job en une seule ligne de code.

nyl (non vérifié), le mar, 29/05/2012 - 11:56

rectification :
dpq est plus complet : je peux voir les "group by" avec lui et pas avec la fonction light que tu mentionnes dans ton article.

Yoran, le mar, 29/05/2012 - 12:16

Alors moi je les vois les "group by" ceci dit. Maintenant dans certains cas, tu peux avoir besoin de faire un $query->preExecute() avant l'appel à __toString().

Ceci étant dit, si tu utilises devel, je ne vois pas l'intérêt de mon astuce. Autant utilisé l'existant.

Yoran, le mar, 29/05/2012 - 12:33
Si cela t'intéresse, dernière version de ma fonction qui ajoute le quoting des valeurs et le fameux preExecute :
} elseif (is_object($mixed) && ($mixed instanceof SelectQuery || $mixed instanceof PagerDefault)) {
   $connection = Database::getConnection();
   $mixed->preExecute();
   $valeurs = $mixed->getArguments();
   foreach ($valeurs as $clef => &$valeur) {
     if (!is_numeric($valeur)) {
       $valeur = $connection->quote($valeur);
     }
   }
   $dump .= strtr((string)$mixed, $valeurs+array('{'=>'', '}'=>''));
 } else {

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...