perf(rating): fetch-join comment tree to kill N+1 in public list (H7)

findApprovedRootsByDoctor used a plain findBy, so Comment::toArray() lazy-loaded
likes, replies and the author per comment (and recursively per reply). Hydrate
in two fetch-join passes (roots + author + likes; then replies + their author +
likes + one further reply level) — no per-comment lazy loads for a two-level
thread.

Regression: tests/Rating/CommentListNPlusOneTest (functional correctness — like
counts, approved-only replies, author preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-28 19:35:34 +03:30
co-authored by Claude Opus 4.8
parent eb1997066e
commit 42d934abc2
3 changed files with 92 additions and 5 deletions
+34 -4
View File
@@ -22,10 +22,40 @@ class CommentRepository extends ServiceEntityRepository
/** @return Comment[] approved root comments (no parent) for a doctor */
public function findApprovedRootsByDoctor(Doctor $doctor): array
{
return $this->findBy(
['doctor' => $doctor, 'status' => Comment::STATUS_APPROVED, 'parent' => null],
['createdAt' => 'DESC']
);
// Two fetch-join passes (not the product, to avoid a likes×replies
// cartesian) hydrate everything toArray() touches, so serialization
// triggers no per-comment lazy loads (N+1): pass 1 = roots + author +
// likes, pass 2 = replies + their author + likes, merged into the same
// managed root entities.
$roots = $this->createQueryBuilder('c')
->addSelect('u', 'l')
->leftJoin('c.user', 'u')
->leftJoin('c.likes', 'l')
->where('c.doctor = :doctor')
->andWhere('c.status = :status')
->andWhere('c.parent IS NULL')
->setParameter('doctor', $doctor)
->setParameter('status', Comment::STATUS_APPROVED)
->orderBy('c.createdAt', 'DESC')
->getQuery()
->getResult();
if ($roots !== []) {
// Also initialise each reply's own replies collection (empty in a
// two-level thread) so reply->toArray() doesn't lazy-load it per row.
$this->createQueryBuilder('c')
->addSelect('r', 'ru', 'rl', 'rr')
->leftJoin('c.replies', 'r')
->leftJoin('r.user', 'ru')
->leftJoin('r.likes', 'rl')
->leftJoin('r.replies', 'rr')
->where('c IN (:roots)')
->setParameter('roots', $roots)
->getQuery()
->getResult();
}
return $roots;
}
/** @return Comment[] */