Matthieu de Canteloube
Matthieu de Canteloube est architecte chez Theodo et développeur Symfony2 certifié. Il est également co-fondateur de Vimies, un réseau social mobile de partage de vidéos.
Vous pouvez le retrouver sur twitter: @matts2cant.
Depuis sa création il y a maintenant 4 ans, Doctrine 2 s'est imposé comme l'ORM standard sur toutes les applications Symfony2. Doctrine 2 est un outil très utile fournissant une couche d'abstraction pour la manipulation des bases de données relationnelles. Mais la clarté et la facilité de développement ont cependant un coût non négligeable. C'est pourquoi après le développement de certaines fonctionnalités, il est parfois nécessaire d'opérer une passe d'optimisation afin de gagner en performances.
Il ne s'agit pas ici de vous faire tomber dans l'écueil de l' « optimisation prématurée », mais plutôt de vous sensibiliser à certaines pratiques qui peuvent améliorer grandement les performances de vos applications.
Différents outils existent afin d'identifier les parties de votre application qui sont ralenties à cause de Doctrine et, qui par conséquent, nécessitent une optimisation.
Cet outils possède l'avantage d'être disponible de manière native dans toute installation de « Symfony2 Standard Edition ». Il présente un certain nombre de métriques très utiles pour évaluer la performance de Doctrine sur une page donnée. On y trouvera entre autres les métriques suivantes :
EXPLAIN
sur chaque requête afin d'identifier les lenteurs éventuelles (index manquant, etc...),En complément du profiler, il existe un certain nombre d'outils permettant de surveiller les performances d'une application dont New Relic qui possède l'avantage d'être conçu pour fonctionner en production. Il permet notamment d'identifier les pages les plus lentes, puis de voir quelle partie du code pose problème. Il est ensuite plus facile d'entreprendre une action corrective pour améliorer les performances.
Des outils de profilage comme xdebug et xh-prof permettent également d'identifier les goulots d'étranglement en terme de performances.
Il existe un ensemble de règles et de bonnes pratiques à garder en tête afin d'éviter 80% des problèmes de performance.
Doctrine 2 propose la possibilité de récupérer une entité en relation avec une autre entité grâce à un accesseur. C'est bien entendu très pratique mais il ne faut pas perdre de vue que, lors de l'utilisation d'un « getter », Doctrine effectue alors une requête SQL pour récupérer l'entité liée.
Ce comportement devient notamment problématique dans les pages de listing. Prenons un exemple concret :
Imaginons une page qui affiche une liste de commentaires sur un article d'un site e-commerce. Chaque commentaire est accompagné du nom de son auteur (« le message à été posté par ... »).
La méthode la plus commune pour implémenter cette fonctionnalité serait :
{% for message in messages %}
<div class="comment">
{{ message.text }}
<small>
Posted by {{ message.author.name }}
</small>
</div>
{% endfor %}
Or, dans une page de listing comportant plusieurs centaines d'éléments, cela se traduira en autant de requêtes SQL vers la base de données. Ce grand nombre de requêtes pourra être évité en utilisant par exemple l'option fetch="EAGER"
ou en passant par une méthode d'un « repository ».
Lors de la création d'une entité, il est recommandé de mettre en place des indexes SQL afin d'accélérer le temps de recherche sur les colonnes de la base de données.
Voici le code correspondant dans Doctrine :
/**
* @Entity
* @Table(name="user",indexes={
* @Index(name="email_idx", columns={"email"})
* })
*/
class User
{
...
}
On parle de fuite de requêtes lorsque le nombre de requêtes sur une page n'est pas fixe. Ce genre de problème n'est pas nécessairement grave dans la mesure ou le nombre de requêtes reste borné, mais peut par contre poser problème si le nombre de requêtes augmente de façon incontrôlée.
Il est à noter que le profiler Symfony2 affiche un avertissement quand le nombre de requêtes sur une page devient anormalement élevé.
Doctrine possède un mécanisme puissant de cache permettant d'accélérer de manière non négligeable ses performances.
Le cache peut être activé à 3 niveaux :
La configuration permettant d'activer le cache Doctrine dans Symfony2 est la suivante (pour memcache) :
# app/config/config_prod.yml
Doctrine:
orm:
entity_managers:
default:
metadata_cache_driver:
type: memcache
host: localhost
port: 11211
instance_class: Memcache
query_cache_driver:
type: memcache
host: localhost
port: 11211
instance_class: Memcache
Il existe aussi des implémentations pour apc
.
EAGER
fetch »Dans certains cas, il peut être nécessaire de spécifier l'option fetch="EAGER"
dans la configuration d'une relation Doctrine afin de diminuer le nombre de requêtes :
class User {
...
/**
* @ManyToOne(targetEntity="Group", cascade={"all"}, fetch="EAGER")
*/
private $group;
...
}
Cette option permet de demander à Doctrine d'aller récupérer automatiquement les entités correspondant à cette relation lors de l'appel aux méthodes find
, findOne
, findOneBy
... des « repositories ».
Cela fait notamment sens lors d'une relation du type « l'entité A possède l'entité B ».
Cette option est néanmoins à utiliser avec précautions, notamment pour les relations comportant un grand nombre d'éléments.
Il est possible de récupérer plusieurs entités différentes en une seule requête Doctrine. Cela peut d'ailleurs présenter un gain de temps très conséquent. Pour ce faire, il suffit d'utiliser la méthode addSelect
de l'objet QueryBuilder
:
class UserRepository
{
...
public function findUsersWithGroups()
{
$qb = $this
->createQueryBuilder('user')
->addSelect('group')
->leftJoin('user.groups', 'group')
->where(...)
;
return $qb->getQuery()->execute();
}
...
}
Dans cet exemple, lors de l'exécution de la requête, Doctrine récupérera une liste d'utilisateurs ainsi que leurs groupes associés. Ils pourront alors être appelés de manière classique grace à la méthode getGroups
de la classe User
.
Il ne faut pas perdre de vue que, même s'ils permettent dans la majorité des cas d'améliorer les performances d'une application, les cas d'optimisation couverts dans cet article restent cependant relativement génériques.
Dans des cas extrêmes, il sera parfois nécessaire de continuer plus en profondeur le travail d'optimisation en utilisant par exemple des requêtes SQL natives plutôt que de passer par le language DQL de Doctrine.
Voici un exemple simple de l'utilisation d'une requête SQL native avec Doctrine.
// Get connection
$conn = $entityManager->getConnection();
// Get table name
$meta = $entityManager->getClassMetadata(User::class);
$tableName = $meta->getTableName();
// Get random ids
$sql = "SELECT id AS id FROM $tableName WHERE active = true ORDER BY RAND()";
$statement = $conn->executeQuery($sql);
$fetchedIds = array_map(function ($element) {
return $element['id'];
}, $statement->fetchAll());
return $fetchedIds;
On peut remarquer dans l'exemple ci-dessus que même si Doctrine n'est pas utilisé de manière traditionnelle, il est toujours possible de récupérer les métadonnées d'une entité afin de générer une requête SQL qui s'adaptera au mapping Doctrine 2.
Vous trouverez plus d'informations sur les requêtes SQL natives sur la documentation officielle.