Requête natives avec Symfony 2 et doctrine.

Quand on est développeur, à chaque fois qu’on résout un problème, c’est pour en trouver un autre derrière.

Je sortais à peine de ma dernière galère que voilà que j’ai replongé dans une nouvelle : les requête avec Doctrine.

Oui, parce que Doctrine, c’est beau, ça brille, ça donne envie. Mais… Non. Le nouveau constructeur de requête ne tiens pas ses promesses et on atteint très très vite ses limites. Contrairement à son ancienne version qui montrait une grande robustesse et une grande simplicité, vous aurez probablement très vite besoin d’utiliser avec la version 2 ce qu’on appelle des « requêtes natives » (native query dans la langue de Shakespeare).

mais bon, comme vous vous en foutez un peu de ma vie, ne prolongeons pas plus cette introduction.

Le constructeur de requêtes

Doctrine nous offre comme outil principal le constructeur de requête (query builder). Il est très simple et fonctionne très bien pour des requête basique :

$query = $this->createQueryBuilder('u')
->addSelect("SUM(CASE WHEN kComp.slug is not null THEN 1 ELSE 0 END)")
->leftJoin('u.keywords', 'kComp', \Doctrine\ORM\Query\Expr\Join::WITH, '1 = 1')
->where('u.id <> :userId')
->groupBy('u.id')
->orderBy('u.lastLogin', 'ASC')
->setParameter('userId', $_search->getUser()->getId())
;

Donc comme on peut le voir, ça fourni quand même toutes les fonctionnalités de bases :

  • ajout de champs personnalisés,
  • jointures strictes via innerJoin ou jointures flottantes via leftJoin, avec possibilités d’ajouter des conditions supplémentaire via le mot clef « WITH »
  • les conditions where
  • les classiques « order by »
  • et enfin l’utilisation de paramètres personnalisés avec la méthode « setParameter » qui remplacera automatiquement les variables par leurs valeurs.

Seulement, de nombreuse fonction ne sont pas reconnues, comme par exemple le « IF » mysql. De plus, Doctrine ne permet pas (plus) d’écraser les conditions ON ce qui s’avère beaucoup plus bloquant.

C’était pourtant ce qui rendaient Doctrine 1 vraiment plaisant à utiliser, on pouvait simplement écraser les jointures ON avec « ->innerJoin(‘u.Relation ON u.condition = « new_condition »‘« . Ce n’est malheureusement plus possible.

Alors comment faire quand on veut exécuter une requête un poil plus intéressante ? Et bien c’est le chapitre suivant…

Les requêtes natives

Doctrine nous fournie pour se faire la possibilité de lancer directement des requêtes natives. Malheureusement, elles sont beaucoup moins « user friendly », surtout si vous utilisez Symfony 2.3 ou inférieur.

La première chose à faire dans ce cas, c’est de mettre à jour l’ORM vers la version 2.4. En effet, Symfony utilise une version inférieur de l’ORM par défaut, et elle est bourrée de bugs au niveau des requêtes natives, ce qui les rend inutilisables.

Ouvrez donc le fichier composer.json à la racine de votre projet et modifiez-le pour redéfinir les dépendances tel que :

"require": {
...,
"doctrine/orm": "~2.4",
...,

Ensuite, ouvrez un terminal et lancez la commande ./composer.phar update pour effectuer la mise à jour de la nouvelle version de l’ORM Doctrine.

Maintenant, nous pouvons enfin créer notre requête native :

// init vars
$where = '';
$parameters = array();

// tout d'abords, nous créons le builder pour mapper les résultats en entités doctrine compréhensible :
$rsm = new ResultSetMappingBuilder($this->_em);
$rsm->addRootEntityFromClassMetadata('My\UserBundle\Entity\User', 'u'); // on définie l'entité de base qui apparaîtra dans le FROM
$rsm->addScalarResult('compatibility', 'compatibility'); // on déclare notre champ personnalisé
$rsm->addJoinedEntityFromClassMetadata('My\UserBundle\Entity\Keyword', 'kComp', 'u', 'keywords', array(
'id' => 'kCompid',
'title' => 'kComptitle',
)); // et enfin, on ajoute les relations que l'on désire récupérer. Notez qu'il faut obligatoirement renommer manuellement les champs qui ont le même nom que ceux que l'on pourrait trouver dans l'entité principale, ici "id" et "title".

// maintenant, nous allons définir des conditions manuellement
if ($_search->getTitle() !== null) {
$where .= ' AND u.title = :title';
$parameters['title'] = $_search->getTitle();
}

// On rédige la requête en sql. On insère l'objet de mapping des résultat en tant que select et les conditions WHERE. Remarquez qu'on peut maintenant tout à fait écraser les jointures et en créer plusieurs.
$sql = 'SELECT ' . $rsm . ', SUM(CASE WHEN kComp.slug = kSearch.slug THEN 1 ELSE 0 END) as compatibility
FROM `fos_user` u
LEFT JOIN keyword kSearch ON kSearch.user_id = :userId
LEFT JOIN keyword kComp ON kComp.user_id = u.id AND kComp.slug = kSearch.slug
WHERE u.id <> :userId ' . $where . '
GROUP BY u.id
ORDER BY u.last_activity ASC'
;

// Il ne reste plus qu'à créer l'objet de requête native et à lui passer les paramètres qu'on a défini plus haut.
$query = $this->_em->createNativeQuery($sql, $rsm);
$query->setParameters($parameters);
$query->setParameter('userId', $_search->getUser()->getId());

Voilà, maintenant vous pouvez récupérer le résultat de votre requête correctement hydraté en objet Doctrine !


Remerciement à FrozenFire du channel irc #doctrine pour son aide 😉