perf(secretary,billing): kill N+1 in secretary + claims lists (M8, M9)

M8: DoctorSecretary::toArray() lazy-loaded secretary/doctor/clinic per row;
fetch-join them in findByDoctorScope/findByClinic (shared listWithRelations()).

M9: enrichClaims() lazy-loaded each claim's items collection and called
insuranceRepo->find() per claim. Fetch-join items in findByTenant (Paginator,
fetchJoinCollection) and batch-fetch insurance names once.

Regressions (query count constant vs row count): SecretaryListNPlusOneTest,
ClaimsListNPlusOneTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-28 20:43:35 +03:30
co-authored by Claude Opus 4.8
parent 61ac775175
commit ccb71e4371
6 changed files with 141 additions and 14 deletions
+10 -4
View File
@@ -4,6 +4,7 @@ namespace App\Billing\Repository;
use App\Billing\Entity\Claim;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry;
class ClaimRepository extends ServiceEntityRepository
@@ -24,12 +25,17 @@ class ClaimRepository extends ServiceEntityRepository
*/
public function findByTenant(string $entityType, int $entityId, array $filters = [], int $page = 1, int $limit = 50): array
{
return $this->tenantQb($entityType, $entityId, $filters)
// fetch-join items so enrichClaims()/toArray() don't lazy-load the
// items collection per claim (N+1). fetchJoinCollection keeps the LIMIT
// paginating by claim.
$qb = $this->tenantQb($entityType, $entityId, $filters)
->addSelect('i')
->leftJoin('c.items', 'i')
->orderBy('c.id', 'DESC')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit)
->getQuery()
->getResult();
->setMaxResults($limit);
return iterator_to_array(new Paginator($qb, fetchJoinCollection: true));
}
public function countByTenant(string $entityType, int $entityId, array $filters = []): int