Le problème N+1 et les stratégies de fetch
Le piège classique avec Doctrine reste le chargement paresseux mal maîtrisé. Itérer sur une collection OneToMany sans précaution déclenche une requête SQL par entité liée. La solution consiste à contrôler explicitement le fetch via DQL ou les attributs de mapping.
public function findWithOrders(): array
{
return $this->createQueryBuilder('c')
->addSelect('o')
->leftJoin('c.orders', 'o')
->getQuery()
->getResult();
}
addSelect est essentiel ici : sans lui, le leftJoin sert uniquement à filtrer, Doctrine hydrate quand même les commandes en requêtes séparées.
Fetch EAGER : à éviter par défaut
Définir fetch: 'EAGER' sur une association peut sembler pratique, mais Doctrine charge alors systématiquement la relation, même quand elle n'est pas utilisée, et cela peut créer des jointures en cascade difficiles à prévoir. Préférez le lazy loading associé à des requêtes DQL explicites selon le contexte d'usage.
UnitOfWork et le cycle de vie des entités
Comprendre l'UnitOfWork évite bien des surprises. Doctrine ne synchronise rien tant que flush() n'est pas appelé : il compare l'état actuel des entités trackées à un instantané pris au chargement (change tracking par snapshot comparison).
- MANAGED : l'entité est suivie par l'EntityManager.
- NEW : créée mais jamais persistée.
- DETACHED : plus suivie (après un
clear()par exemple). - REMOVED : marquée pour suppression au prochain flush.
Un appel à flush() sans argument synchronise toutes les entités managées, pas seulement celle que vous venez de modifier. Sur un traitement par lots, cela peut coûter cher : pensez à vider périodiquement l'EntityManager avec clear() pour libérer la mémoire.
Migrations et cohérence du schéma
DoctrineMigrationsBundle génère des diffs à partir des métadonnées de mapping, mais ne détecte pas tout (renommage de colonne interprété comme suppression + ajout, par exemple). Relisez toujours les migrations générées avant de les exécuter en production.
Cache de requêtes et de résultats
Doctrine distingue le query cache (mise en cache du DQL compilé en SQL) du result cache (mise en cache des résultats eux-mêmes). Le second doit être utilisé avec prudence : une entité modifiée hors du cycle Doctrine (SQL brut, autre process) ne l'invalidera pas automatiquement.
Repository custom vs QueryBuilder inline
Évitez de construire des requêtes complexes directement dans les contrôleurs ou services métier. Centraliser la logique de requête dans des méthodes de repository dédiées facilite les tests et la réutilisation, tout en gardant les entités focalisées sur leur seule responsabilité de mapping.