[Symfony] Une astuce simple pour améliorer les performances de Doctrine

Contexte sur la performance

Saviez-vous que les performances des applications Symfony peuvent être améliorées en configurant efficacement vos requêtes avec Query::HINT_FORCE_PARTIAL_LOAD ?

Lors du développement d’applications Symfony sous Doctrine, le chargement complet d’objets peut parfois entraîner des ralentissements indésirables. Notamment pour les objets avec de nombreuses dépendances ou clés étrangères.

Pour réduire considérablement les lenteurs dues à ces chargements et améliorer l’expérience utilisateur des applications Symfony, nos experts ont découvert une solution simple pour pallier à ce problème avec l’option de Doctrine : Query::HINT_FORCE_PARTIAL_LOAD.

Un cas de base pour Symfony

Imaginez que vous vous trouvez face à un projet complexe comprenant plusieurs tables interconnectées. Lorsqu’une page est appelée, Doctrine charge les données des entités nécessaires pour cette page. Cependant, si ces entités ont de nombreuses relations, Doctrine générera un grand nombre de requêtes à la base de données, ce qui rallongera le temps de chargement de la page et impactera négativement l’expérience utilisateur.

Prenons un exemple fictif de projet qui servira de fil conducteur dans cet article. Les différentes tables et relations de la base de données sont les suivantes :

  • Utilisateurs : contenant des informations telles que l’id, le nom, le prénom, l’adresse et le code postal.
  • Commandes : avec des champs tels que le numéro, l’id de l’utilisateur, l’id du produit, la quantité et le prix. Les commandes sont liées aux tables Utilisateurs et Produits.
  • Produits : comprenant des détails tels que l’id, le numéro, le nom, la description, le type et le prix. Les produits sont liés aux tables Commandes.
  • Animaux : contenant des informations sur les animaux tels que l’id, le nom, le commentaire, la race, le type et le poids. Les animaux sont liés à la table Utilisateurs.
  • Animaux_races : établissant une relation entre les animaux et les races.
  • Races : avec des détails sur les races et leurs caractéristiques.

Notre objectif est de concevoir une page qui affiche uniquement les utilisateurs accompagnés de leurs animaux, tout en appliquant un filtre en fonction des races. Dans la mise en œuvre initiale, Doctrine effectuera une requête pour chaque utilisateur sur les commandes, suivie d’une autre requête pour les animaux associés. En cas de présence de 1000 utilisateurs, cela entraînera la génération de 2000 requêtes à la base de données. L’utilisation de Query::HINT_FORCE_PARTIAL_LOAD permet d’éviter cette surcharge.

Dans cet article, vous découvrirez comment nous boostons les performances de l’application avec cette approche en évitant le chargement excessif de data et les appels inutiles à la base de données, tout en fluidifiant vos requêtes SQL.

Doctrine Query::HINT_FORCE_PARTIAL_LOAD : Comprendre et optimiser le chargement d’Objets

Query::HINT_FORCE_PARTIAL_LOAD est une fonctionnalité de Doctrine qui permet de contrôler précisément le chargement des objets lors de l’exécution des requêtes SQL. Il est employé dans le cas de tables interconnectées au sein d’une base de données, ou lors de la manipulation d’objets lourds.

Son objectif est d’optimiser les performances en ne chargeant que les champs requis et en évitant le gaspillage des ressources. Dans notre exemple précédent, où seules certaines données liées aux utilisateurs, animaux et races étaient pertinentes pour la page, cette fonctionnalité permet de charger uniquement l’essentiel.

Objet complétement chargé vs Objet partiellement chargé

Une application Symfony qui exécute un chargement complet, récupère des données parfois inutiles d’un objet et va ralentir considérablement l’application car elle va charger chaque propriété de chaque objet ce qui est coûteux en termes de performances.

En activant Query::HINT_FORCE_PARTIAL_LOAD, vous indiquez à Doctrine de charger uniquement les données spécifiées par la requête pour chaque objet, évitant ainsi le chargement d’informations superflues et réduisant la charge sur le système. Dans notre cas d’exemple nous avons seulement besoin de charger les données d’utilisateurs, animaux et races.

Toutes les données de l’objet, même celles non nécessaires pour la tâche actuelle.

Objet partiellement chargé

Seules les données essentielles à la tâche actuelle sont chargées, optimisant ainsi les performances.


Notre étude de cas illustre un défi courant dans le développement web : l’impact des requêtes excessives sur les performances d’une application.

Avec 32 requêtes pour charger une page, notre application Symfony souffrait d’un ralentissement majeur. La racine du problème résidait dans la gestion inefficace des objets fortement interconnectés par des relations et des clés étrangères.
La solution apportée était l’implémentation de Query::HINT_FORCE_PARTIAL_LOAD dans Doctrine. 
Cette stratégie a permis de configurer le chargement des objets de manière à récupérer uniquement les données nécessaires, éliminant ainsi le fardeau des données superflues. 
Ce changement a réduit considérablement le nombre de requêtes dans ce cas, de 32 à seulement 6 et, par conséquent, amélioré la réactivité de l’application.
Cette étude de cas démontre l’importance de l’optimisation des requêtes et la puissance de Doctrine pour améliorer les performances des applications Symfony.

Voyons désormais comment l’activer et l’utiliser stratégiquement pour alléger la charge de vos applications.

Comment mettre en place Query::HINT_FORCE_PARTIAL_LOAD : Technique et Astuces

Dans le développement d’une application Symfony, l’efficacité des requêtes est cruciale pour des performances optimales et ce paramètre de Doctrine ORM est notre allié pour affiner les requêtes SQL, en ne chargeant que les données strictement nécessaires. 

Un code optimisé est synonyme d’une meilleure expérience pour l’utilisateur final, voyons donc comment mettre en place Query::HINT_FORCE_PARTIAL_LOAD.

php
namespace App\Repository;

class AnimalRepository extends EntityRepository
{
    // ...

    public function getAnimauxDesUtilisateurs($race1, $race2)
    {
        $entityManager = $this->getEntityManager();
        $queryBuilder = $entityManager->createQueryBuilder();
		
        return $queryBuilder->select(['utilisateur', 'animaux', 'races'])
            ->from('App\Entity\Utilisateur', 'utilisateur')
            ->join('utilisateur.animaux', 'animaux')
            ->join('animaux.races', 'races')
            ->orderBy('utilisateur.prenom')
            ->groupBy('utilisateur.id, animaux.id')
            ->getQuery()
            ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
            ->getResult();
    }
}

Dans cet exemple, la fonction getAnimauxDesUtilisateurs récupère les utilisateurs avec leurs animaux en se concentrant uniquement sur les informations essentielles, grâce à l’utilisation de Query::HINT_FORCE_PARTIAL_LOAD.

Le résultat de cette fonction génère une collection d’utilisateurs accompagnés de leurs animaux et de leurs races, étant donné leur présence dans la sélection. Notons que les informations liées aux commandes ne sont pas chargées, comme souhaité.. Voici un aperçu du contenu renvoyé par la fonction :

array:2 [
  0 => Utilisateur {#1
    -id: 1
    -nom: "John"
    -prenom: "Doe"
    -adresse: null
    -code_postale: null
    -animaux: Doctrine\ORM\PersistentCollection {#10
      -snapshot: array:2 [
        0 => Animaux {#8
          -id: 1
          -nom: "Max"
          -commentaire: "Chien adorable"
          -races: Doctrine\ORM\PersistentCollection {#12
            -snapshot: array:1 [
              0 => Races {#16
                -id: 1
                -race: "Golden Retriever"
                -caracteristiques: "Caractéristiques pour Golden Retriever"
              }
            ]
          }
          -types: "Chien"
          -poids: 30
        }
        1 => Animaux {#9
          -id: 2
          -nom: "Whiskers"
          -commentaire: "Chat joueur"
          -races: Doctrine\ORM\PersistentCollection {#13
            -snapshot: array:1 [
              0 => Races {#17
                -id: 2
                -race: "Siamese"
                -caracteristiques: "Caractéristiques pour Siamese"
              }
            ]
          }
          -types: "Chat"
          -poids: 12
        }
      ]
      commandes: null
    }
  }
  1 => Utilisateur {#21
    -id: 2
    -nom: "Alice"
    -prenom: "Smith"
    -adresse: null
    -code_postale: null
    -animaux: Doctrine\ORM\PersistentCollection {#22
      -snapshot: array:2 [
        0 => Animaux {#20
          -id: 3
          -nom: "Polly"
          -commentaire: "Chient bavard"
          -races: Doctrine\ORM\PersistentCollection {#24
            -snapshot: array:1 [
              0 => Races {#25
                -id: 1
                -race: "Golden Retriever"
                -caracteristiques: "Caractéristiques pour Golden Retriever"
              }
            ]
          }
          -types: "Chient"
          -poids: 35
        }
        1 => Animaux {#26
          -id: 4
          -nom: "Buddy"
          -commentaire: "Chat câlin"
          -races: Doctrine\ORM\PersistentCollection {#27
            -snapshot: array:1 [
              0 => Races {#28
                -id: 2
                -race: "Siamese"
                -caracteristiques: "Caractéristiques pour Siamese"
              }
            ]
          }
          -types: "Chat"
          -poids: 15
        }
      ]
      commandes: null
    }
  }
]

Pour une utilisation efficace, assurez-vous de spécifier les champs ou les relations dont vous avez réellement besoin. Les objets partiellement chargés peuvent rendre le code fragile, notamment lorsque des champs critiques ou des relations essentielles sont exclus du chargement. Par exemple, l’appel d’une fonction sur une entité non chargée (comme getId()) entraînera une erreur. Faites attention à charger toutes les informations nécessaires à votre logique ou à utiliser cette fonctionnalité uniquement dans des scénarios de lecture où la modification des objets n’est pas nécessaire.

Il est à noter toutefois que les objets partiellement chargés doivent être gérés avec prudence. Car ils peuvent nécessiter un rafraîchissement complet pour synchroniser leur état avec la base de données.

Autre considération importante à prendre en compte dans le cadre de chargement partiel : les problèmes de cache et la reconnaissance inappropriée des objets partiels par Doctrine.

Conclusion sur les performances

En résumé, la mise en œuvre de chargements partiels d’objets est une optimisation significative pour les applications utilisant des objets complexes. Bonne nouvelle, cela peut être mis en place relativement facilement avec des bénéfices immédiats sur les performances.

Dans notre étude de cas, nous avons constaté une amélioration significative de 81.25% suite à l’utilisation de la technique prouvée et non négligeable de Query::HINT_FORCE_PARTIAL_LOAD de Doctrine ! En effet, cela permet à la fois d’améliorer et d’affiner la réactivité et l’efficience des applications Symfony tout en réduisant les coûts de performance liés à la surcharge de données.

Appliquez nos conseils et assurez-vous que votre application Symfony fonctionne à son plein potentiel. Pour des solutions personnalisées ou des conseils d’experts, contactez notre équipe. Nous sommes à votre disposition pour vous aider à optimiser vos projets Symfony.