Optimize N+1 queries: batch billets, soldCounts, paid orders with items

- AccountController: single query for all billets by categories, single
  GROUP BY query for sold counts, eager-load items on paid orders
- HomeController: single query for all buyable billets of active categories

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-22 20:13:31 +01:00
parent 0772e618da
commit 0e79c65966
3 changed files with 40 additions and 16 deletions

View File

@@ -47,7 +47,7 @@
- [x] Rate limiting sur les routes sensibles (login 5/15min, commande 10/5min, invitation 5/15min, contact 3/10min)
- [x] CSRF token sur tous les formulaires POST (auto-inject + auto-verify)
- [x] Cache Meilisearch : invalider quand un événement est modifié (déjà fait via EventIndexService::indexEvent)
- [ ] Optimiser les requêtes N+1 (stats tab, billets par catégorie)
- [x] Optimiser les requêtes N+1 (stats tab, billets par catégorie)
### Tests
- [ ] Atteindre 90%+ de couverture PHP

View File

@@ -374,13 +374,29 @@ class AccountController extends AbstractController
['position' => 'ASC'],
);
$allBillets = $em->getRepository(Billet::class)->findBy(['category' => $categories], ['position' => 'ASC']);
$billets = [];
$billetIds = [];
foreach ($allBillets as $billet) {
$catId = $billet->getCategory()->getId();
$billets[$catId][] = $billet;
$billetIds[] = $billet->getId();
}
$soldCounts = [];
foreach ($categories as $category) {
$categoryBillets = $em->getRepository(Billet::class)->findBy(['category' => $category], ['position' => 'ASC']);
$billets[$category->getId()] = $categoryBillets;
foreach ($categoryBillets as $billet) {
$soldCounts[$billet->getId()] = $em->getRepository(BilletOrder::class)->count(['billet' => $billet]);
if ($billetIds) {
$rows = $em->createQueryBuilder()
->select('IDENTITY(bo.billet) AS billetId, COUNT(bo.id) AS cnt')
->from(BilletOrder::class, 'bo')
->where('bo.billet IN (:ids)')
->setParameter('ids', $billetIds)
->groupBy('bo.billet')
->getQuery()
->getArrayResult();
foreach ($rows as $row) {
$soldCounts[$row['billetId']] = (int) $row['cnt'];
}
}
@@ -390,7 +406,16 @@ class AccountController extends AbstractController
: $em->getRepository(BilletBuyer::class)->findBy(['event' => $event], ['createdAt' => 'DESC']);
$eventOrders = $paginator->paginate($ordersQuery, $request->query->getInt('page', 1), 20);
$paidEventOrders = $em->getRepository(BilletBuyer::class)->findBy(['event' => $event, 'status' => BilletBuyer::STATUS_PAID]);
$paidEventOrders = $em->createQueryBuilder()
->select('o', 'i')
->from(BilletBuyer::class, 'o')
->leftJoin('o.items', 'i')
->where('o.event = :event')
->andWhere('o.status = :status')
->setParameter('event', $event)
->setParameter('status', BilletBuyer::STATUS_PAID)
->getQuery()
->getResult();
$eventStats = $this->computeEventStats($paidEventOrders);
return $this->render('account/edit_event.html.twig', [

View File

@@ -147,16 +147,15 @@ class HomeController extends AbstractController
['position' => 'ASC'],
);
$activeCategories = array_filter($categories, fn (Category $c) => $c->isActive());
$allBillets = $activeCategories ? $em->getRepository(Billet::class)->findBy(
['category' => $activeCategories, 'notBuyable' => false],
['position' => 'ASC'],
) : [];
$billets = [];
foreach ($categories as $category) {
if (!$category->isActive()) {
continue;
}
$categoryBillets = $em->getRepository(Billet::class)->findBy(
['category' => $category],
['position' => 'ASC'],
);
$billets[$category->getId()] = array_filter($categoryBillets, fn (Billet $b) => !$b->isNotBuyable());
foreach ($allBillets as $billet) {
$billets[$billet->getCategory()->getId()][] = $billet;
}
return $this->render('home/event_detail.html.twig', [