Mettre en place du eager loading

Petit nouveau ! | 7 Messages

24 juil. 2016, 02:23

Bonjour à tous,

Je me pose actuellement une question, je voudrais mettre en place du eager loading lorsque je fais des requêtes.
Dans mon cas j'ai une table Post, Tag, PostTag, vous vous doutez donc qu'un Post peut avoir plusieurs Tag.

Actuellement je récupère une Collection de Post et les tags associés comme ceci :
SELECT 
    Post.idPost,
    Post.name,
    Post.slug,
    Post.content,
    GROUP_CONCAT(Tag.name) AS tags
FROM Post
  
LEFT JOIN PostTag
    ON PostTag.idPost = Post.idPost
    
LEFT JOIN Tag
    ON Tag.idTag = PostTag.idTag
    
GROUP BY Post.idPost
ORDER BY Post.idPost DESC;
Une fois la requête exécuté je parcours les résultats via un foreach et j'instancie dedans un objet PostViewModel et j'hydrate les données dedans, voici le cas concret :
<?php

namespace Blog\View\Model;

class PostViewModel
{
    /** @var int */
    public $id;
    /** @var string */
    public $name;
    /** @var string */
    public $slug;
    /** @var string */
    public $content;
    /** @var array */
    public $tags;
}
<?php

namespace Blog\Infrastructure\Finder;

use Blog\View\Model\PostViewModel;
use Illuminate\Support\Collection;

class PostFinder extends AbstractFinder
{
    /**
     * Retourne la liste des articles
     *
     * @param string $order
     * @return Collection
     */
    public function findAll($order = 'DESC')
    {
        $select = "
            SELECT 
                Post.idPost,
                Post.name,
                Post.slug,
                Post.content,
                GROUP_CONCAT(Tag.name) AS tags
            FROM Post
              
            LEFT JOIN PostTag
                ON PostTag.idPost = Post.idPost
                
            LEFT JOIN Tag
                ON Tag.idTag = PostTag.idTag
                
            GROUP BY Post.idPost
            ORDER BY Post.idPost {$order};  
        ";

        $statement = $this->db->createStatement($select);
        $result = $statement->execute();

        $posts = new Collection();

        if ($result->isQueryResult() === false
            || $result->count() < 1
        ) {
            return $posts;
        }

        foreach ($result as $row) {
            $post = new PostViewModel();
            $this->hydrate($row, $post);
            $posts->push($post);
        }

        return $posts;
    }

    /**
     * @param array $row
     * @param PostViewModel $post
     */
    private function hydrate(array $row, PostViewModel $post)
    {
        $post->id = (int)$row['idPost'];
        $post->name = $row['name'];
        $post->slug = $row['slug'];
        $post->content = $row['content'];
        $post->tags = $row['tags'] !== null
            ? (array)explode(',', $row['tags'])
            : null;
    }
}
Ce que je souhaiterais faire c'est une requête qui récupère tous les Post :
SELECT 
    Post.idPost,
    Post.name,
    Post.slug,
    Post.content
FROM Post;
Et ensuite une autre qui ajouterait dans la collection des Posts l'attributs tags qui contiendrait mes tags sous forme d'objet TagViewModel avec les propriétés id et name.

J'avoue que je ne sais pas trop comment faire ici.

J'ai un début de requête :
SELECT
    Tag.idTag,
    Tag.name,
    PostTag.idPost
FROM Tag

LEFT JOIN PostTag
    ON PostTag.idTag = Tag.idTag

WHERE PostTag.idPost IN (:ids)
:ids est récupéré quand je parcours ma collection, je pousse les ids dans un tableau.

Comment procéderiez-vous ?

Pour plus d'infos vous pouvez consulter le dépôt github

Merci d'avance.

ViPHP
ViPHP | 928 Messages

25 juil. 2016, 16:57

N'utilisant pas Laravel, aucune idée. Mais ça a l'air d'être expliqué dans leur documentation ici : https://laravel.com/docs/5.1/eloquent-r ... er-loading

Petit nouveau ! | 7 Messages

25 juil. 2016, 19:15

Je n'utilise pas Laravel... j'ai pris le terme eager loading mais rien de plus. J'utilise ZF2 mais même en utilisant un framework ou pas, je posais la question concernant la mise en place de quelque chose comme cela.

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

26 juil. 2016, 12:56

salut,

Soit tu fais une requête sans le group_concat qui rammène tout le monde et tu gère un tableau avec les tags (gestion de l'unicité du tag etc.) que tu insère à la fin.
Soit tu fais une seconde requête pour avec la liste des tag correspondant à ton Post.
Avec une "vraie" requête préparée tu ne devrais pas avoir de problème (si tu en as il faudra avoir un cache coté service.

A partir de ta requête et en virant la jointure qui sert à rien
SELECT  idTag,
    name
FROM Tag
WHERE idTag IN (select idTag from PostTag where idPost = :idPost)
execute cette requête pour chaque ligne de résultat de la requête
SELECT 
    Post.idPost,
    Post.name,
    Post.slug,
    Post.content
FROM Post;
(faut p'tet un order by ? :) )

je ne sais pas ce que tu utilises pour la connexion SGBD mais PDOStatement propose une méthode fetchAll (fetch style = PDO::FETCH_CLASS et la classe en second argument) qui permet d'avoir ta liste d'objet tout prêts ;)

@+
Il en faut peu pour être heureux ......

Petit nouveau ! | 7 Messages

31 juil. 2016, 13:43

Merci pour ta réponse @moogli

Voici ce que j'avais fait avant que tu poste ton message : dépôt github

Ton idée de virer la jointure sur PostTag et de faire un WHERE IN (select ... ) est intéressante. Je suppose que niveau performances c'est mieux ?

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

01 août 2016, 09:42

je ne pense pas que cela change grand chose, après il faudrait faire un explain pour voir la chose.
et de toutes façon tu n'as pas fraiment je choix. Soit tu fait un select sur la table de liaison (donc PostTag) et tu utilises deux jointures pour avoir les informations, soit tu utilises ce que j'ai mis dans mon premier message.
La requête avec les jointures
select les,champs 
from PostTag
 join Post using(idPost)
 join Tag using(idTag)
ensuite en php il faut que tu gère la fait que tu va avoir plusieurs lignes pour le post (a cause des tags) il faut donc que tu gères le post courant pour lui ajouter les tags (genre un tableau qui prend en clef l'id du post et tu cherches cet id dans le tableau final s'il n'existe pas tu crée le post s'il existe tu ajoutes dans la liste des tag).
soit tu fais un peu plus simple en php avec une requête préparée dans la boucle de traitement du select sur la table Post.
C'est simple à comprendre et efficace sachant que de toute façon cela ne sert à rien d'afficher 5 millions de post le problème de performance est très limités, si tu as trop de post de toutes façon tu utilise une pagination et personne ne parcourt un page de 4km (a part peux être les bots d'indexation comme ceux de google mais je crois bien qu'ils savent utiliser les liens XD).

Pense quand même a désactiver l’émulation des requêtes préparées dans les options de connexion de PDO.

@+
Il en faut peu pour être heureux ......