Des astuces Doctrine/DQL

De e-glop
Révision datée du 8 janvier 2011 à 16:24 par BeTa (discussion | contributions) (La solution retenue)

Lorsque vous travaillez avec l'ORM Doctrine, il est parfois difficile de retrouver toute la souplesse de l'écriture en Raw SQL. C'est un peu normal : abstraction oblige.

Ajouter des parenthèses arbitraires

En particulier pour l'exception des conditions ->andWhereIn() / ->whereIn()

Le cas des parenthèse via Doctrine en DQL mériterait une petite amélioration, peut-être Doctrine v2 ira plus loin dans ce sens. Ici nous allons étudier comme pouvoir rajouter des parenthèses pour un des rares cas où il n'est pas possible de passer par une écriture "simple" en SQL.

Si vous souhaitez donner des priorités particulières à certaines conditions en DQL, voici une solution :

Présentation du problème

Erreur "simple"

Dans cet exemple nous souhaitons obtenir :

 Tous les contacts dont le "postalcode" est 1000
 ET le "name" est "Foo" OU le "firstname" est "Bar"

Nous allons donc essayer avec la requête suivante :

 $q = Doctrine::getTable('Contact')->createQuery();
 $q->andWhere('name = ?','Foo')
   ->orWhere('firstname = ?','Bar')
   ->andWhere('postalcode = ?',1000);
 echo $q->getSqlQuery();
 
 // prints something like : SELECT * FROM contact WHERE (name = 'Foo' OR firstname = 'Bar' AND postalcode = 1000)

Pour autant, vous l'aurez compris, ce n'est pas ce que l'on souhaite. Ici en langage courant, on va obtenir :

 Tous les contacts dont le "name" est "Foo"
 OU ALORS
 Tous les contacts dont le "firstname" est "Bar" ET dont le "postalcode" est 1000

Solution "simple"

Pour corriger cela, nous allons construire la requête différemment, un peu plus "bas niveau" :

 $q = Doctrine::getTable('Contact')->createQuery();
 $q->andWhere('name = ? OR firstname = ?',array('Foo','Bar'))
   ->andWhere('postalcode = ?',1000);
 echo $q->getSqlQuery();
 
 // prints something like : SELECT * FROM contact WHERE ((name = 'Foo' OR firstname = 'Bar') AND postalcode = 1000)

Nous obtenons alors bien le résultat escompté.

Erreur "complexe" avec whereIn()

Les choses deviennent plus complexes lorsque vous utiliserez des conditions ->andWhereIn(). reprenons alors un autre problème pour illustrer cela :

 Tous les contacts dont le "postalcode" est 1000
 ET le "name" est dans la liste XX OU le "firstname" est dans la liste YY

Voilà comment on aurait envie de s'y prendre en DQL :

 $XX = array(...);
 $YY = array(...);
 
 $q = Doctrine::getTable('Contact')->createQuery();
 $q->andWhereIn('name',$XX)
   ->orWhere('firstname',$YY)
   ->andWhere('postalcode = ?',1000);
 echo $q->getSqlQuery();
 
 // prints something like : SELECT * FROM contact WHERE (name IN (X..X) OR firstname IN (Y..Y) AND postalcode = 1000)

Cela nous donne le même type de problème que rencontré précédemment.

Les diverses solutions

Plusieurs solutions existent :

  • patcher directement Doctrine_Query pour ajouter une fonctionnalité "parenthèses"
    • Avantage : ne nécessite aucune évolution sur votre fond de code
    • Inconvénient : vous "forkez" de fait avec le code officiel de Doctrine, maintenance très très complexe (déconseillé)
  • Étendre la classe Doctrine_Query pour y ajouter la fonctionnalité "parenthèse" recherchée
    • Avantage : propre et net... utilise bien toute la puissance d'un langage objet
    • Inconvénient : demande à revoir tout votre fond de code pour utiliser votre classe personnalisée... dans le cas de Symfony par exemple, cela peut vite devenir très très complexe, voire même de devoir forker en retouchant le code officiel de Symfony

La solution retenue

Cette solution n'est pas très très belle, mais elle a le mérite d'être simple. La voici :

rajouter une condition ->andWhere('(TRUE') au début de votre bloc de conditions et finir par ->andWhere('TRUE)') à la fin.

 $q = Doctrine::getTable('Contact')->createQuery();
 $q->andWhere('(TRUE');
   ->andWhereIn('name',$XX)
   ->orWhere('firstname',$YY)
   ->andWhere('TRUE)')
   ->andWhere('postalcode = ?',1000);
 echo $q->getSqlQuery();
 
 // prints something like : SELECT * FROM contact WHERE ((TRUE AND name IN (X..X) OR firstname IN (Y..Y) AND TRUE) AND postalcode = 1000)

Cela nous donne bien le resultat escompté.