Symfony 1.3 & 1.4, dernière ligne droite !

Bonjour à tous !

Comme prévu, les versions RC2 de symfony 1.3 & 1.4 sont sorties aujourd’hui. Vous pouvez les télécharger ici pour la 1.3 et ici pour la 1.4. N’hésitez pas à les tester dès aujourd’hui et à faire vos retours !

Les versions stables devraient sortir la semaine prochaine.

La question qui se pose maintenant : quelle version choisir ?
- la 1.3 : support 1an, stuff déprécié mais pas supprimé
- la 1.4 : support 3 ans, stuff déprécié supprimé (il faut penser à faire un point complet sur tous ses projets …)

Nous avons fait le choix de la 1.4 pour le support de 3 ans principalement. En effet, pour des professionnels, il est important de pouvoir se dire que notre outil sera maintenu longtemps. On ne sait pas si on pourra le mettre à jour fréquemment !
Du coup, cela demande un peu plus de travail avant la migration afin de faire un point complet sur l’ensemble des projets déjà créés pour être sûr de ne rien oublier (stuff déprécié) et du coup, de mettre à jour ce stuff.

Il faut également savoir pour ceux étant en 1.2 que la migration en 1.3 ou 1.4 est importante et plus ou moins urgente afin de garder une version maintenue. Peu le savent mais le support de la version 1.2 a été augmenté de 3 mois, ce qui le porte jusqu’en février 2010 (initialement prévu jusqu’à la release de la 1.3). Vous avez donc un peu plus de temps pour faire vos tests de migration donc profitez-en !

A bientôt pour de nouvelles aventures !

Pourquoi optimiser les requêtes Doctrine ?

Bonjour à tous !

Et oui, me revoilà après plusieurs mois sans post. Il faut dire qu’entre le boulot, les cours du soir et le sport, le temps est assez limité pour le reste ! Mais bon, je profite d’avoir un petit moment pour vous faire partager une expérience qui, pour beaucoup, semble évidente mais à laquelle on ne pense pas suffisament : Optimiser une requête Doctrine

Dans l’admin generator, on pense souvent à modifier la requête de sélection de Doctrine (pour l’affichage de la liste) en intégrant des leftJoin ou innerJoin (ceux qui ne le font pas encore, pensez-y !).

Je le fais toujours mais dans mes modules de frontend (en général, de l’affichage de données en base), je ne le faisais pas (sans doute parce que mes requête étaient simples).
Sur un projet, j’ai pu découvrir le bénéfice non négligeable d’optimiser la requête d’affichage des données, que ce soit au niveau du nombre de requêtes mais également au niveau du temps d’affichage de la page (plus précisément, au niveau du temps de calcul de l’actin d’index).

Pour que ce soit plus parlant, regardons quelques printscreen :
- utilisation de la requête générée par Doctrine

Requête non optimisée

- utilisation d’une requête (et d’une action) optimisée

Voilà, je pense que c’est suffisamment parlant non ?
Pour information, j’ai rafraichi plusieurs fois la page afin d’obtenir des valeurs lorsque le cache est utilisé.

En non optimisé, Doctrine effectue 129 requêtes (3.98ms) et une fois optimisée, UNE seule requête est effectuée (0.01ms).
Le temps étant en ms, c’est assez négligeable certes mais pour une base de données énorme avec beaucoup d’accès, le ratio est de 400 ! De plus, le fait d’avoir un certain nombre de requêtes (hydratation de base Objet) rentre en compte dans le temps de calcul de l’action dont elle dépend et ici, le ratio est encore important 6 !

Afin de vous ouvrir un peu plus les yeux, il faut voir le schema de la base afin de comprendre la « complexité » des relations :

#schema.yml
Etat:
  tableName: etat
  columns:
    titre:
      type: string(255)
      notnull: true
    color:
      type: string(255)
      notnull: true
Comment:
  actAs:
    Timestampable: ~
  tableName: comment
  columns:
    titre:
      type: string(255)
    comment:
      type: clob
      notnull: true
    user_id:
      type: integer(4)
  relations:
    sfGuardUser:
      type: one
      foreignType: many
      local: user_id
      foreign: id
      foreignAlias: Users
Tache:
  actAs:
    Timestampable: ~
  tableName: tache
  columns:
    activite:
      type: string(255)
      notnull: true
    id_etat:
      type: integer
      notnull: true
  relations:
    Etat:  { onDelete: CASCADE, local: id_etat, foreign: id }
    TacheComments:
      class: Comment
      refClass: TacheComment
      local: id_tache
      foreign: id_comment
    sfGuardUsers:
      class: sfGuardUser
      refClass: TacheUser
      local: id_tache
      foreign: id_user
TacheUser:
  tableName: tache_user
  columns:
    id_tache:
      type: integer
      notnull: true
    id_user:
      type: integer(4)
      notnull: true
  relations:
    Tache:  { onDelete: CASCADE, local: id_tache, foreign: id }
    sfGuardUser: { onDelete: CASCADE, local: id_user, foreign: id }
TacheComment:
  tableName: tache_comment
  columns:
    id_tache:
      type: integer
      notnull: true
    id_comment:
      type: integer
      notnull: true
  relations:
    Tache:  { onDelete: CASCADE, local: id_tache, foreign: id }
    Comment: { onDelete: CASCADE, local: id_comment, foreign: id }

Voici également la requête optimisée (je ne sélectionne que les données nécessaires et j’hydrate en array afin de limite les ressources nécessaires et sachant qu’il s’agit d’affichage de données, c’est parfait !) :

// TacheTable.class.php
  public function retrieveTacheWithJoins(){
    return Doctrine_Query::create()
          ->select('
              t.*,
              e.*,
              tc.id_comment,
              tu.id_user,
              c.titre, c.comment, c.user_id, c.updated_at,
              cu.username,
              u.username
            ')
          ->from('Tache t')
          ->leftJoin('t.Etat e')
          ->leftJoin('t.TacheComment tc')
          ->leftJoin('tc.Comment c')
          ->leftJoin('c.sfGuardUser cu')
          ->leftJoin('t.TacheUser tu')
          ->leftJoin('tu.sfGuardUser u')
          ->execute(array(), Doctrine::HYDRATE_ARRAY);
  }

Et pour finir, je vous propose les deux actions (en premier, l’action optimisée avec l’hydratation array et en second, l’ancienne action avec l’hydratation objet) :

//actions.class.php
  public function executeIndex(sfWebRequest $request)
  {
    $type = $request->getParameter('type');
    $this->etat = $request->getParameter('etat');
    $this->taches = Doctrine::getTable('Tache')->retrieveTacheWithJoins();
    $this->tab = array();
    $this->comments = array();
    foreach($this->taches as $tache){
      $this->users = array();
      foreach($tache['TacheUser'] as $user){
        $this->users[] = $user['sfGuardUser']['username'];
      }
      $this->users = implode(" ", $this->users);
      $this->tab[$tache['id']] = array(
                                        'activite' => $tache['activite'],
                                        'etat' => $tache['Etat']['titre'],
                                        'etat_color' => $tache['Etat']['color'],
                                        'users' => $this->users,
                                    );

      foreach($tache['TacheComment'] as $comment){
        $this->comments[$tache['id']][] = array(
                                              'user' => $comment['Comment']['sfGuardUser']['username'],
                                              'titre' => $comment['Comment']['titre'],
                                              'date' => date('d/m/Y', strtotime($comment['Comment']['updated_at'])),
                                              'comment' => $comment['Comment']['comment'],
                                            );
      }
    }
    $this->pdf = new pdf('L');
    $this->setTemplate($type);
  }

  public function executeOldIndex(sfWebRequest $request)
  {
    $type = $request->getParameter('type');
    $this->etat = $request->getParameter('etat');
    $this->taches = Doctrine::getTable('Tache')->findAll();
    $this->tab = array();
    $this->comments = array();
    foreach($this->taches as $tache){
      $this->users = array();
      foreach($tache->TacheUser as $user){
        $this->users[] = $user->getSfGuardUser();
      }
      $this->users = implode(" ", $this->users);
      $this->tab[$tache->getId()] = array(
                                        'activite' => $tache->getActivite(),
                                        'etat' => $tache->getEtat(),
                                        'etat_color' => $tache->getEtat()->getColor(),
                                        'users' => $this->users,
                                    );

      foreach($tache->TacheComment as $comment){
        $this->comments[$tache->getId()][] = array(
                                              'user' => $comment->getComment()->getSfGuardUser(),
                                              'titre' => $comment->getComment()->getTitre(),
                                              'date' => date('d/m/Y', strtotime($comment->getComment()->getUpdatedAt())),
                                              'comment' => $comment->getComment()->getComment(),
                                            );
      }
    }
    $this->pdf = new pdf('L');
    $this->setTemplate($type);
  }

Voilà, je pense que c’est assez explicite, j’obtiens exactement le même rendu avec l’une ou l’autre des actions et des requêtes et j’avoue avoir été vraiment impressionné sur ce coup là !
Morale : toujours chercher à optimiser ses requêtes !