Divagations sur les jointures avec SPIP

jeudi 29 décembre 2011

GIS / boucles / jointures

Afin d’optimiser les performances du plugin GIS j’ai voulu simplifier le code des squelettes qui génèrent le JSON qui alimente les cartes du plugin. Le squelette original contenait deux boucles imbriquées comme ceci :

<BOUCLE_art(ARTICLES){id_article ?}{id_rubrique ?}{id_secteur ?}{id_mot ?}{id_auteur ?}{recherche ?}{0, #ENV{limit}}{","}><BOUCLE_gis(GIS){id_article}{","}>

http://zone.spip.org/trac/spip-zone/browser/_plugins_/gis/branches/v2/json/gis_articles.html?rev=55738

On va donc tenter d’utiliser qu’une seule boucle avec une jointure au lieu des deux boucles imbriquées.

Essai 1

On utilise les jointures "classiques" dans la boucle. Il faut juste faire attention à respecter la casse du nom des tables car la mise en capitales du nom des tables occasionne une perte de jointure [1].

<BOUCLE_gis(ARTICLES gis_liens gis)
        {id_article ?}
        {id_rubrique ?}
        {id_secteur ?}
        {id_mot ?}
        {id_auteur ?}
        {recherche ?}{","}>

Requête SQL générée avec id_rubrique=1 passé dans l’url :

SELECT articles.id_article, 0 as points, L3.lon, L3.lat, articles.titre, articles.descriptif, articles.lang
FROM spip_articles AS `articles`  
INNER JOIN spip_gis AS L3 ON ( L3.id_gis = articles.id_gis )
WHERE (articles.statut = 'publie')
        AND (articles.date < '2138-01-01 00:00:00')
        AND (articles.id_rubrique = 1)
GROUP BY articles.id_article

On voit bien que la jointure ne se fait pas sur les bons champs :

INNER JOIN spip_gis AS L3 ON ( L3.id_gis = articles.id_gis )

L’ajout du critère {gis.id_gis ?} nous permet de forcer la jointure sur la table gis.

<BOUCLE_gis(ARTICLES gis_liens gis)
        {gis.id_gis ?}
        {id_article ?}
        {id_rubrique ?}
        {id_secteur ?}
        {id_mot ?}
        {id_auteur ?}
        {recherche ?}{","}>

Requête SQL générée avec id_rubrique=1 passé dans l’url :

SELECT articles.id_article, 0 as points, L2.lon, L2.lat, L1.id_gis, articles.titre, articles.descriptif, articles.lang
FROM spip_articles AS `articles`  
INNER JOIN spip_gis_liens AS L1 ON ( L1.id_objet = articles.id_article AND L1.objet='article')
INNER JOIN spip_gis AS L2 ON ( L2.id_gis = L1.id_gis )
WHERE (articles.statut = 'publie')
        AND (articles.date < '2138-01-01 00:00:00')
        AND (articles.id_rubrique = 1)
GROUP BY articles.id_article

Requête SQL générée avec id_auteur=9 passé dans l’url :

SELECT articles.id_article, 0 as points, L2.lon, L2.lat, L1.id_gis, articles.titre, articles.descriptif, articles.lang
FROM spip_articles AS `articles`  
INNER JOIN spip_auteurs_articles AS L4 ON ( L4.id_article = articles.id_article )
INNER JOIN spip_gis_liens AS L1 ON ( L1.id_objet = articles.id_article AND L1.objet='article')
INNER JOIN spip_gis AS L2 ON ( L2.id_gis = L1.id_gis )
WHERE (articles.statut = 'publie')
        AND (articles.date < '2138-01-01 00:00:00')
        AND (L4.id_auteur = 9)
GROUP BY articles.id_article

Requête SQL générée avec id_article=54 passé dans l’url :

SELECT articles.id_article, 0 as points, L2.lon, L2.lat, L1.id_gis, articles.titre, articles.descriptif, articles.lang
FROM spip_articles AS `articles`  
INNER JOIN spip_gis_liens AS L1 ON ( L1.id_objet = articles.id_article AND L1.objet='article')
INNER JOIN spip_gis AS L2 ON ( L2.id_gis = L1.id_gis )
WHERE (articles.statut = 'publie')
        AND (articles.date < '2138-01-01 00:00:00')
        AND (articles.id_article = 54)
GROUP BY articles.id_article

Le GROUP BY des requêtes ne nous permet d’afficher tous les éléments si plusieurs points sont attachés à un même article. Pour corriger cela on utilise le critère {fusion champ_sql} [2] afin de modifier la clause GROUP BY de la requête :

<BOUCLE_gis(ARTICLES gis_liens gis)
        {gis.id_gis ?}
        {id_article ?}
        {id_rubrique ?}
        {id_secteur ?}
        {id_mot ?}
        {id_auteur ?}
        {fusion gis_liens.id_gis}
        {recherche ?}{","}>

Requête SQL générée avec id_article=54 passé dans l’url :

SELECT L1.id_gis, articles.id_article, 0 as points, L2.lon, L2.lat, articles.titre, articles.descriptif, articles.lang
FROM spip_articles AS `articles`  
INNER JOIN spip_gis_liens AS L1 ON ( L1.id_objet = articles.id_article AND L1.objet='article')
INNER JOIN spip_gis AS L2 ON ( L2.id_gis = L1.id_gis )
WHERE (articles.statut = 'publie')
        AND (articles.date < '2138-01-01 00:00:00')
        AND (articles.id_article = 54)
GROUP BY articles.id_article,L1.id_gis

On remarque que les jointure générées par SPIP sont correctes, mais il reste un problème. Il ne sera pas possible d’afficher le titre et le descriptif du point GIS car des champs homonymes sont présents dans la table spip_articles (première table de la boucle). En effet, notre squelette de départ utilisait la notation des balises non ambigües pour afficher le titre du point GIS sinon celui de l’article :

[(#TITRE*|sinon{#_art:TITRE*}|supprimer_numero|json_encode)]

Et comme SPIP ne permet pas de notation du type #gis.TITRE comme on pourrait le voir en SQL, cette solution ne convient pas [3].

Essai 2

Cette fois on boucle directement sur la table spip_gis et on provoque les jointures entre les tables SQL en utilisant la notation TABLE.NOM dans nos critères [4].

<BOUCLE_gis(GIS)
        {id_article ?}
        {articles.id_rubrique ?}
        {articles.id_secteur ?}
        {mots_articles.id_mot ?}
        {auteurs_articles.id_auteur ?}
        {recherche ?}{","}>

Requête SQL générée avec id_rubrique=1 passé dans l’url :

SELECT gis.id_gis, 0 as points, gis.lon, gis.lat, gis.titre, gis.descriptif
FROM spip_gis AS `gis`  
INNER JOIN spip_gis_liens AS L2 ON ( L2.id_gis = gis.id_gis )
INNER JOIN spip_articles AS L3 ON ( L3.id_article = L2.id_objet AND L2.objet='article')
WHERE (L3.id_rubrique = 1)
GROUP BY gis.id_gis

On remarque que les champs de la table spip_articles ne sont pas présents dans le SELECT. Voilà pourquoi il faut déclarer l’ensemble des tables pour faire une jointure si on souhaite afficher des champs d’une autre table [5]. On ne pourra donc pas afficher le titre et le descriptif de nos articles avec cette solution.

Essai 3

Cette fois on va créer un critère perso qui forcera les jointures vers les tables gis_liens/gis et qui nous permettra aussi d’afficher les informations de l’article.

function critere_gis_dist($idb, &$boucles, $crit) {
       
        $boucle = &$boucles[$idb];
        $id_table = $boucle->id_table; // articles
        $primary = $boucle->primary; // id_article
        $objet = objet_type($id_table); // article
       
        // ajouter le titre et le descriptif du point au select
        $boucle->select[]= 'gis.titre AS titre_gis';
        $boucle->select[]= 'gis.descriptif AS descriptif_gis';
        // jointure sur spip_gis_liens/spip_gis
        // cf plugin notation
        // $boucle->join["surnom (as) table de liaison"] = array("surnom de la table a lier", "cle primaire de la table de liaison", "identifiant a lier", "type d'objet de l'identifiant");
        $boucle->from['gis_liens'] = 'spip_gis_liens';
        $boucle->join['gis_liens']= array("'$id_table'","'id_objet'","'$primary'","'gis_liens.objet='.sql_quote('$objet')");
        $boucle->from['gis'] = 'spip_gis';
        $boucle->join['gis']= array("'gis_liens'","'id_gis'");
        // bien renvoyer tous les points qui son attachés à l'objet
        $boucle->group[] = 'gis_liens.id_gis';
        // ajouter gis aux jointures et spécifier les jointures explicites pour pouvoir utiliser les balises de la table de jointure
        // permet de passer dans trouver_champ_exterieur() depuis index_tables_en_pile()
        // cf http://article.gmane.org/gmane.comp.web.spip.zone/6628
        $boucle->jointures[] = 'gis';
        $boucle->jointures_explicites = 'gis_liens gis';
       
}

Puis on utilise ce critère dans notre boucle.

<BOUCLE_gis(ARTICLES)
        {gis}
        {id_article ?}
        {id_rubrique ?}
        {id_secteur ?}
        {id_mot ?}
        {id_auteur ?}
        {recherche ?}{","}>

Requête SQL générée avec id_rubrique=1 passé dans l’url :

SELECT gis.titre AS titre_gis, gis.descriptif AS descriptif_gis, articles.id_article, 0 as points, gis.lon, gis.lat, gis_liens.id_gis, articles.titre, articles.descriptif, articles.lang
FROM spip_articles AS `articles`  
INNER JOIN spip_gis_liens AS gis_liens ON ( gis_liens.id_objet = articles.id_article AND gis_liens.objet='article')
INNER JOIN spip_gis AS gis ON ( gis.id_gis = gis_liens.id_gis )
WHERE (articles.statut = 'publie')
        AND (articles.date < '2138-01-01 00:00:00')
        AND (articles.id_rubrique = 9)
GROUP BY gis_liens.id_gis,articles.id_article
LIMIT 0,5000

Dans cette boucle, la balise #TITRE affichera logiquement le titre des articles. Il ne nous manque plus que deux balises pour permettre d’afficher le titre et le descriptif des points GIS.

function balise_titre_gis_dist($p) {
        return rindex_pile($p, 'titre_gis', 'gis');
}

function balise_descriptif_gis_dist($p) {
        return rindex_pile($p, 'descriptif_gis', 'gis');
}

Et hop, on peut maintenant écrire ça dans nos squelettes :

[(#TITRE_GIS*|sinon{#TITRE*}|supprimer_numero|json_encode)]

Tout ce travail a été intégré au plugin GIS à partir de la version 2.2.0 (voir le commit sur la zone).

Répondre à cet article

Qui êtes-vous ?
Ajoutez votre commentaire ici
  • Ce formulaire accepte les raccourcis SPIP [->url] {{gras}} {italique} <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.