Comment Doctrine2 ORM a tué le bénéfice de mon cache pour soulager ma base de donnée

Je travaille sur une application web, qui comme beaucoup, va chercher des données quasi invariables en base. Dans mon cas la liste des pays. On peut dire que cette liste est plutôt stable. Il devient donc moins intéressant de récupèrer les données dans MariaDB, mais pour la cohérence des autres données stockées en base je ne veux pas remplacer la table "pays" en base par autre chose.
J'ai donc décidé de mettre l'ensemble de la table "pays" en cache dans Redis.  J'ai besoin des infos de la table "pays" sur toutes mes pages voire plusieurs fois par page, le gain devrait donc être intéréssant.
Pour limiter l'impact dans mon code déjà existant, j'implémente NormalizableInterface et DenormalizableInterface dans l'entity Pays et c'est toute l'entity Doctrine en JSON que je stocke dans Redis. Je modifie ma méthode me permettant de retrouver un pays pour utiliser Redis avec un fallback vers MariaDB si ma donnée a expirée. Dans tous les cas je retourne l'entity Pays correctement hydratée.

Après quelques tests, je voie bien que Redis est utilisé, géniale, je pousse mon code en prod. Je suis scotché à mes outils de monitoring, curieux de voir le gain et là, c'est un peu la douche froide, mon gain est très loin de ce que j'avais estimé.
Après analyse des logs, je me rend compte que j'ai encore beaucoup de requête SQL vers la table "pays" en plus des hits sur Redis.

L'analyse me montre 2 choses:

  • Tous les hits Redis que je fais et que je n'utilise que pour de l'affichage dans le navigateur, j'économise 100% des requêtes SQL. L'entity ne provient pas de Doctrine mais je ne l'utilise pas avec Doctrine non plus, donc aucun problème.
  • Tous les hits Redis que je fais pour re-utiliser Doctrine après, j'économise 0% des requêtes SQL. Systématiquement, j'ai une requête SQL qui va chercher les données en base.

Mon application faisant beaucoup plus de persistance que d'affichage (sorte de back office), j'ai au final plombé les performances car pour quasiment chaque requête SQL, j'ai ajouté une requête Redis et une déserialisation. Je suis partie en stage de spéléologie dans le code de Doctrine2, surtout de l'EntityManager et de l'UnitOfWork, pour comprendre ce qu'il se passe.

Pour prendre en compte mon entity provenant de Redis et éviter une exception de la part de Doctrine me disant que l'entity Pays est inconnue, j'utilise la fonction merge() pour ajouter mon entity à l'EntityManager.

 
$article = new Atricle();
$article->setPays($pays);
...
 
$em->merge($pays);
$em->persist($article);
$em->flush($article);

Mon problème est dans le fonctionnement de cette méthode merge(). La méthode merge() de l'EntityManager fait quasiment proxy vers la méthode merge() de l'UnitOfWork. En lisant le code on voit vite que Doctrine essaye de remplacer notre entity $pays par sa version précédemment détachée. Sauf que provenant de Redis, cette entity n'a jamais existé dans l'UnitOfWork. Ce dernier fait donc un find() pour obtenir une entity attachable. Je n'ai pas investigué plus pour savoir pourquoi il ne pouvait pas se contenter de ce que je lui ai donné.

Tout ça étant un peu embettant, j'ai tenté une autre approche en utilisant une référence à l'entity. Ce qui me donne le code:

 
$paysRef = $em->getReference('MC\AppBundle\Entity\Pays', $pays->getId());
 
$article = new Atricle();
$article->setPays($paysRef);
...
 
$em->persist($article);
$em->flush($article);

Malheureusement au moment de l'appel de persist(), Doctrine fait de nouveau un find(). 

Je me retrouve donc bloqué, avec mon entity qui provient de Redis et l'ORM de Doctrine qui systèmatiquement veut aller chercher l'entity lui même via la base. Je n'ai pas investigué sur l'utilisation du cache dans Doctrine pour remplacer mon cache Redis car je veux partager mon cache Redis avec d'autres applications qui n'utilisent pas Doctrine.
La seule solution que j'ai trouvé est de ne pas utiliser l'ORM pour la persistence et de tout gérer à la main via Doctrine dbal, plus d'EntityManager ni d'UnitOfWork et je peux enfin apprécier des gains de perfs sur mon application.

Il y a 4 commentaires.

Ecrit par maarek_j le 23 févr. 2016

Si tu as besoin seulement de la partie cache redis pour la lecture. Tu peux simplement utiliser le ResultCache de Doctrine ORM. http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/caching.html Ça évite de faire une implementation custom de cache et de jongler entre la version cache et la version en base. Ça permet de rester dans une logique SQL. Et ça permet d'utiliser tout les providers fournis par doctrine/cache (memory, redis, filesystem, memcache etc...)

Réponse de Ulrich le 23 févr. 2016

C'est une très bonne solution, mais dans mon cas devant partager le cache avec une autre application, ça m'était impossible.

Ecrit par Michael le 27 janv. 2016

Article très intéressant, merci d'avoir partagé ces infos ! Comment gères-tu la partie mise en cache / récupération / invalidation dans Redis ?

Réponse de Ulrich le 27 janv. 2016

Je gère le cache "à la main". Je me suis fait un bundle pour Redis via lequel j'accède à mon cahce. SI tu utilises l'ORM de Doctrine tu devrais pouvoir automatiser via les events postPersist, postUpdate et postDelete. Par contre si tu utilises dbal, il te faut taa propre gestion.

Ajouter un commentaire