jeu, 15/07/2010 - 08:50 | Yoran   Articles Drupal SQL views

Injecter son propre code SQL dans Views

database1.s600x600.jpg

Je ne vais pas revenir sur mon amour immodéré pour views, ce n'est plus trop la peine. Views est ce qu'il est et il arrive parfois que le choix de l'utiliser ne se pose pas, il est là, enraciné dans un projet, indéboulonnable sous peine d'exploser les charges. Et à chaque modification un peu conséquente c'est la même histoire, un temps de dingue à trifouiller en tout sens cette interface maudite pour obtenir à grand coup de prévisualisation une requête que j'ai en tête depuis le début. Passé un moment, on se lasse d'une telle gymnastique et je me suis donc mis à chercher, sans grande conviction, un moyen d'injecter mes propres requêtes dans Views. Et la bonne nouvelle est que oui, c'est faisable !

Pour quoi faire ?

Au delà de l'allergie atavique, de nombreuses bonnes raisons peuvent amener à vouloir injecter son propre code SQL. Déjà parce que certains exercices de style deviennent rapidement infaisables avec le générateur de requêtes (imbrications de select, group by complexes, etc.). Ensuite pour gagner du temps, car nombreux sont ceux qui vont tout de même plus vite à écrire du SQL qu'à click-clicker en tout sens. En revanche si certains comptaient par là améliorer les performances de views, qu'ils ne se fassent pas trop d'illusions. La génération de requête n'est pas, et loin de là, le poste le plus dispendieux de l'établissement. La production du markup à travers la jungle de couches passe-plat (styles de vue, style de ligne, etc.) arrive clairement en tête, et les joyeuseries dans la gestion du requêtage (voir le coup du count_query un peu plus loin) ne font que rallonger la sauce.

Altération de l'objet Query

Avant d'attaquer l'injection de pur code SQL dans nos vues, nous allons nous arrêter sur une technique plus simple qui peut déjà permettre de dynamiser la requête de manière plus complexe qu'avec l'interface graphique. Le principe de base de cette approche est d'exploiter le hook hook_views_query_alter dont le but est de permettre à un module tiers de modifier l'objet requête ($query, classe Query) d'une vue donnée ($view, classe View) avant que le code SQL ne soit généré. Query est une classe de Views qui représente symboliquement la future requête SQL. Vous y trouverez une série de champs contenant tout ce que vous avez défini dans l'interface graphique (relationships, where, orderby, etc).

Le mieux est comme pour un hook_form_alter d'utiliser la commande var_dump(...) pour déterminer ce que vous désirez changer dans la structure. A titre d'exemple, nous pourrions ensuite faire ceci :

 function mon_module_views_query_alter(&$view, &$query) {
   if($view->name=="ma_vue") {
    // A décommenter pour savoir ce que $query contient
    // echo "<pre>"; var_dump($query); exit();
     if ($_SESSION['tri_par_titre']) {
       $query->orderby [0] = 'node_revisions_title DESC';    
    } else {
       $query->orderby [0] = 'users_name ASC';        
   }
}  
Modification dynamique de l'objet Query avant génération du code SQL

Ici nous changions donc la clef de tri en fonction du contenu d'une variable de session. Le premier paramètre $views contient l'objet "vue" avec sa propriété $view->name que nous utilisons pour vérifier que l'on modifie bien la bonne vue. Le paramètre $query contient l'objet de classe Query représentant ce que vous avez défini dans l'IHM de Views. Pour info $query est tout simplement le champ $view->query. L'intérêt de le passer en paramètre m'échappe donc un peu.

Cette technique fonctionne bien, mais reste très liée à la manière dont views formalise les requêtes. Voyons comment descendre un cran plus bas niveau.

Écrasement de la requête SQL générée par Views

La méthode la plus simple pour injecter du vrai code SQL dans Views, est lisible ici dans la langue de Shakespeare. Le principe est d'exploiter un autre hook de l'API Views, hook_views_pre_execute. Ce hook étant invoqué juste avant l'appel à db_query nous laisse l'opportunité de changer la requête SQL qui a déjà été généré par Views à partir de l'objet Query. Voyons directement un exemple d'implémentation :

function mon_module_views_pre_execute(&$view) {
   if($view->name=="ma_vue") {
         // Décommenter pour voir à quoi ressemble la requête générée par Views
         // echo "<pre>"; var_dump($view->build_info['query']); exit();
         $view->build_info['query']=="
          SELECT
             node.nid AS nid,
             node_revisions.title AS node_revisions_title,
             users.uid AS users_uid,
             users.name AS users_name
          FROM bla bla bla"
;
   }
}
Modification de la requête SQL de views, avant son exécution

La propriété $view->build_info est un tableau de trois clefs : query pour la requête SQL, count_query pour la même requête "optimisée" (on y reviendra) pour le comptage des éléments, et query_args qui est un tableau d'arguments (penser ici aux paramètres d'une fonction db_query) appliqués aux deux précédentes requêtes.

Vous n'avez donc plus qu'à écraser la valeur de query comme dans l'exemple donné plus haut. La seule contrainte est que les champs exposés dans le select aient les mêmes noms que ceux de la requête qu'aurait produit Views. C'est encore plus vrai si vous reprenez un projet existant et que vous n'avez pas envie d'aller trifouiller dans les templates.

Du coup, un bon point de départ pour notre code SQL nous est fourni par le panneau de prévisulatisation de views. Attention cependant, la requête visible dans ce panneau n'est pas toujours strictement celle qui serait générée par Views (vive la cohérence de cet outil..). La méthode la plus fiable est un petit var_dump($query->build_info['query']) dans le "if" de l'implémentation donnée plus haut.

Si la requête que vous injectez ne renvoie pas le même nombre d'éléments que celle d'origine, vous devez en injecter une seconde dans $query->build_info['count_query']. C'est en effet à ce genre de détail que l'on constate avec effroi l'efficacité de Views qui effectue systématiquement deux requêtes, même lorsqu'aucune pagination ne justifie ce comptage.

Mais plus drole encore, lorsque vous construisez cette requête count_query, ne cherchez pas à placer là-dedans un select count(*) from ... car cela ne marcherait pas. En effet, pour une raison qui me dépasse, Views utilise le champ count_query de la manière la plus barbare qui soit, en l'encapsulant dans un select count(*) from (LA_REQUETE_COUNT_QUERY). Super sympa non ? ;-)

En patchant Views

La méthode précédente fonctionne très bien mais me pose un petit problème. Déjà que Views est aussi lent qu'un facteur suisse, l'obliger à générer une requête que l'on sait pertinemment devoir écraser juste après n'est pas très acceptable.

Malheureusement il n'existe pas de hook dans l'API de views pour surcharger la génération du code SQL. Il va donc nous falloir tailler dans le vif et patcher. Le meilleur endroit que j'ai trouvé est le fichier includes/view.inc. Ce dernier contient la classe View, et plus particulièrement la méthode build($display_id = NULL) qui fait très exactement ce qui nous intéresse. En effet, à la ligne 649 (de la version 6.x-2.11) se trouve le code responsable de la génération du code SQL. Nous allons donc modifier cette procédure de sorte à permettre à un module tiers de sa propre mouture :

// Let modules modify the query just prior to finalizing it.
foreach (module_implements('views_query_alter') as $module) {
  $function = $module . '_views_query_alter';
  $function($this, $this->query);
}

// { YB PATCH - Don't generate queries if it's already done
if (empty($this->build_info['query'])) {
// } YB PATCH

$this->build_info['query'] = $this->query->query();
$this->build_info['count_query'] = $this->query->query(TRUE);
$this->build_info['query_args'] = $this->query->get_where_args();

// { YB PATCH - Allow a module to generate its own SQL
}
// } YB PATCH  
patch de la classe View

On a fait plus complexe comme patch, avouez. Le principe est d'exploiter au maximum le hook_views_query_alter que nous avons rencontré plus haut en permettant non plus de modifier l'objet Query, mais de fournir une requête SQL tout prête. Le hack consiste donc pour Views de vérifier qu'une requête n'a pas déjà été injectée, et lancer sa propre génération le cas échéant. Notez au passage que le module une fois hacké sera plus compliqué à mettre à jour sauf si vous utilisez la technique des patchs.

Pour l'implémentation de ce hook, l'approche est strictement la même que pour la technique précédente (Ici aussi vous avez la même contrainte de devoir générer les mêmes champs en nombre et en nommage que ce qu'aurait fait views) a la nuance prés qu'il est cette fois obligatoire de générer aussi le champ count_query.

function mon_module_views_query_alter(&$view, &$query) {
   if($view->name=="ma_vue") {
      $view->build_info['query']="
          SELECT
             node.nid AS nid,
             node_revisions.title AS node_revisions_title,
             users.uid AS users_uid,
             users.name AS users_name
          FROM bla bla bla"
;
      $view->build_info['count_query']="SELECT node.nid AS nid FROM bla bla bla";      
   }
}
Injection du requête SQL à la place de celle de Views

Et voilà :) Simple et efficace

Conclusion

J'espère que cette technique vous sera aussi utile qu'à moi. Si vous n'avez aucune contrainte de performance, vous pouvez vous appuyer sur la méthode douce qui a le mérite de laisser Views intact. Mais dans le cas contraire, le petit hack est un prix bien faible à payer pour retrouver enfin sa liberté.

Commentaires

Nyl auster (non vérifié), le dim, 18/07/2010 - 18:33

mais bon ces trucs ne marchent pas toujours, et il ne reste plus alors qu'à faire sauter le pont de la rivière kwaï.

benoit (non vérifié), le dim, 27/11/2011 - 00:34

Je cherche à faire ça avec D7 et views 3, Vivement votre prochain article !

Selinav (non vérifié), le jeu, 15/07/2010 - 13:43

MErci pour cet excellent article. Quel est vraiment l'intéret d'injecter son sql dans views plutôt que de créer un module perso?

Anthony (non vérifié), le jeu, 15/07/2010 - 15:07

L'interêt d'utiliser cette méthode est expliqué en début de post par Yoran. Dans le cas où tu travail sur un projet existant sur lequel Views est utilisé sur beaucoup de pages. Il est donc préférable d'utiliser cette méthode que de devoir tout réécrire les views dans des modules.

Yoran, le jeu, 15/07/2010 - 16:18

Ah bé voilà, j'ai juste plus rien à dire ;-)

En effet, ce n'est peut-être pas clair mais si tu démarres quelque chose from scratch, l'intérêt de l'approche est à peu prés nul. Mais typiquement sur le projet que je mène en ce moment qui est tartiné de Views du sol au plafond, l'économie de charge est substantielle.

Un autre intérêt potentiel c'est pour pouvoir utiliser sans trop se prendre la tête certains formatteurs évolués comme des timelines ou des cartes, mais là je suis déjà plus circonspect.

 

 

selinav (non vérifié), le jeu, 15/07/2010 - 18:05

Effectivement j'ai lu l'article un peu rapidement...

Mais en tous les cas bravo pour la qualité de l'explication.

Mike (non vérifié), le ven, 16/07/2010 - 15:50

Mon problème est le suivant:

 

J'ai un filtre "A" (valeur numerique)

Et je souhaiterais filtrer avec cette règle : afficher seulement les éléments si A >= (A - (A*0.1))

 

Est-il possible de modifier les valeurs des filtres à partir
de la variable $query ou $view ?

Yoran, le ven, 16/07/2010 - 16:27

Je ne sais pas trop ce que tu entends pas "filtre", je ne suis pas (et ne serais sûrement jamais) un pro de Views. Mais il me semble qu'il y a une étrangeté dans ton énoncé "A >=(A-(A*0.1))", car si je n'ai pas trop perdu en maths ton inégalité est vraie pour toute valeur A <= 0 ;-)

 

Nyl auster (non vérifié), le sam, 17/07/2010 - 14:47

What the hell ?

Mais pour ne pas avoir juste viré la vue plutôt qu'un horrible patch de views suivi d'un hook_views_query_alter (que j'ai déjà utilisé, ce qui est le truc le plus désespéré qu'on puisse faire avec views, à ne faire que sous la menace d'un pistolet) ?

Du coup :

1) tu as patché le module views (mises à jour toussa)

2) l'UI de views va preter à confusion pour le pauvre webmaster qui essaiera de changer un truc, ce qui ne marchera pas puisque tu écrases tout:

3) le code  réel de la requete se trouve dans un hook quelques kilometres plus loind, dans un module.

 

Donc pourquoi ne pas avoir viré la vue en question et remplacée par une requete, qui a recoder les parties qui utilisait une vue ? C'était sans doute plus long mais là c'est un peu de la boucherie quand même non ?

 

Ou mieux, pourquoi ne pas avoir fait un handler de views pour parvenir à faire la requete que tu n'arrivais pas à faire ? c'est le seul moyen propre de s'en sortir si des gens avec des pistolets t'oblige à utiliser views pour faire des requetes qu'il est incapable de réaliser.

Yoran, le sam, 17/07/2010 - 16:04

Pourquoi ne pas avoir virer la vue ? Tu plaisantes ou tu rigoles là ? Tu pourrais aussi bien dire "pourquoi tu n'as pas pris l'option d'exploser tes charges et de manger ta culotte ?" ;-)))) Le problème de remplacer une vue par une requête c'est le coup de la tâche de propre. Si tu replaces views par du db_query, tu ne vas pas générer son markup tout pourris, tu vas faire un truc propre, donc derrière tu vas te frapper toutes les feuilles de styles. Et ce pour toutes les vues que tu modifies. Perso je préfère l'approche douce et les coûts contrôlés, et la rédaction des requêtes est une étape obligatoire que je pourrais ré-utilser.

Mais bon, je peux comprendre la réticence même si "horrible patch" pour juste un pauvre test ajouté, c'est un peu charrier ;-) Personnellement je n'ai que peu d'état d'âme à faire ce genre de chose (surtout avec Views j'avoue ;p) pour peu que ce soit proprement documenté dans la documentation du projet (ce qui est évidement le cas). Perso je double en plus ce genre de précaution avec un hook_help qui permet au pauvre webmestre de s'y retrouver. En plus, un patch bien fait n'a JAMAIS empêché une mise à jour si elle-même se fait par patch différentiel. Le savoir permet de se détendre un peu et de ne pas passer sa vie à attendre que tel ou tel développeur corrige ou améliore leur module. Mais une fois de plus, documenter et documenter encore.

Ceci étant dit, je ne sais pas si je l'ai précisé, mais pour ce fameux super projet fait par une super boite de drupaleux, l'ensemble des vues sont de toute façon codées en dur dans un module custom... Et ce sans hook_help ni documentation, ce qui est encore plus vicieux. Du coup mon hook_views_alter est au même endroit que la définition de la vue elle-même, donc d'un point de vue lisibilité et maintenabilité je n'ai pas dégradé grand chose ;-)

Maintenant ton histoire de handler m'intéresse au plus haut point si elle me permet d'arriver au même résultat de manière "propre". Tu as un pointeur ou un exemple de ce genre de chose ?

 

Nyl auster (non vérifié), le sam, 17/07/2010 - 16:29

je voulais pas avoir l'air agressif hein :)

Vu qu'il s'agit d'un billet de blog visible par tous, je me permets d'avertir que c'est quand même une méthode violente. (et oui pour le css à refaire, bon argument, je n'avais pas pensé à ça...)

 

"Maintenant ton histoire de handler m'intéresse au plus haut point si
elle me permet d'arriver au même résultat de manière "propre". Tu as un
pointeur ou un exemple de ce genre de chose "

J'essaierais de repasser avec un lien sympa, mais tout dépend de la requete que tu essayais de faire, parfois c'est juste pas possible ou ça demande une connaissance de views que seul son developpeur peut avoir sans y passer 3 mois.

 

 

 

Yoran, le sam, 17/07/2010 - 16:39

"je voulais pas avoir l'air agressif hein"

Bouh tu me fais peur, j'espère ne pas t'avoir donné l'impression que je l'avais pris ainsi ! Tes remarques sont parfaitement justifiées. Et même si pour une fois je ne suis pas totalement d'accord, c'est tout l'intérêt pour moi de raconter mes conneries sur un site, avoir du débat contradictoire intéressant (et non poilu ;-)

 

Ok on verra cela pour le handler, mais si ça se trouve, d'ici là, j'aurais craqué et complétement viré Views :-)

 

Nyl auster (non vérifié), le dim, 18/07/2010 - 18:28

salut, ô grand guerrier Drupal qui remet views dans le droit chemin à grand coup de hache, de marteau et de burin.

Bon en fait il n'y a quasiment aucune infos sur comment créer un handlers de views sur internet. Un peu de doc ici mais trois fois rien http://views-help.doc.logrus.com/help/views/api

En gros un handlers c'est juste un objet qui représente un champ dans views (donc normalement faire pour ajouter un champ et les filtres et relations liés à ce champ). Tu peux facilement créer un nouveau hanlder en étendant la classe d'un handler qui existe déjà -voir le dossier de views contenant tous les handlers- et qui se rapproche de ce que tu veux. Du coup tu n'as plus qu'à overrider ou ajouter les méthodes qui t'intéresse.

Une astuce qu'on a souvent utilisée, c'est de créer un handler  (basé sur le champ nid par exemple, à toi de voir selon la situation quel champ doit réprésenter ce handler, sachant qu'un champ peut avoir plusieurs hanlders) en exploitant uniquement la méthode "pre_render".

C'est la méthode qui est appelée tout à la fin, juste avant la sortie du html, de l'éxécution de ta views. Là tu peux caser discrétement une grosse requete sql de la mort - qui ne sera exécuté qu'une fois en tout et pas une fois par résultat - en exploitant toutes les données des champs que views à ramener jusqu'ici.

Par exemple, si views te ramèner 6 "livres", tu disposes de tous leurs nids. Du coup tu peux faire une requete sql (avec un IN sur les nids des livres) à ce moment là pour chercher tous les auteurs des livres; et réinjecter ensuite les bons auteurs avec les bons livres dans les résultats de views.

Je suis pas sur d'être très clair, mais ça nous a sorti de pas mal de galère avec views.

Après le hanlder de permet en thoérie d'overrider n'importe quel méthode de views et donc de modifier les relations, la query etc etc.. mais ça peut plus ou moins rendre fou selon ce que tu essaies de faire. Le mieux c'est de regarder comment fonctionnent les hanlders de views, genre le "views_handler_field_url.inc" dans le dossiers hanlders de views, qui est un exemple simple d'extension d'un hanlder existant.

 

" j'espère ne pas t'avoir donné l'impression que je l'avais pris ainsi !"

pas de souci , C'est toujours un plaisir de lire tes diatribes anti-views, anti-features et anti-panels :)

Nyl auster (non vérifié), le dim, 18/07/2010 - 18:31

j'en profite que pour ajouter vite fait que un truc intéressant de views, c'est comment "déclarer ses tables à views" (cf lien que je t'ai donné). En jouant là dessus, tu peux aller chercher des infos ou lui ajouter des jointures "automatiques" qu'il ne fait pas de base. C'est comme ça que l'ont peut récupérer l'auteur d'un node ou la taxonomie (je ne sais plus) sans avoir besoin de passer par une relationship alors qu'en théorie on devrait. C'est parce que views arrive à se démerder tout seul en nivaguant dans son schéma de définitions de table que lui fournissent les modules. J'ai réussi à me sortir deux fois d'un jointure délicate avec ça, sans avoir à passer par les méchants handlers.

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.
  • Les lignes et les paragraphes vont à la ligne automatiquement.
CAPTCHA
Cette question est là pour déterminer si vous êtes humain ou pas...