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
@@ -38,13 +38,27 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
return $this->findBy(['doctor' => $doctor], ['createdAt' => 'DESC']);
}
/** Base query with secretary/doctor/clinic fetch-joined for toArray() (no N+1). */
private function listWithRelations(): \Doctrine\ORM\QueryBuilder
{
return $this->createQueryBuilder('s')
->addSelect('sec', 'doc', 'cl')
->leftJoin('s.secretary', 'sec')
->leftJoin('s.doctor', 'doc')
->leftJoin('s.clinic', 'cl')
->orderBy('s.createdAt', 'DESC');
}
/** منشی ها مطب شخصی یک دکتر (owner_type='doctor') */
public function findByDoctorScope(Doctor $doctor): array
{
return $this->findBy(
['doctor' => $doctor, 'ownerType' => DoctorSecretary::OWNER_DOCTOR],
['createdAt' => 'DESC']
);
return $this->listWithRelations()
->where('s.doctor = :doctor')
->andWhere('s.ownerType = :type')
->setParameter('doctor', $doctor)
->setParameter('type', DoctorSecretary::OWNER_DOCTOR)
->getQuery()
->getResult();
}
/** رابطه منشی در scope مطب شخصی */
@@ -111,12 +125,11 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
/** همه منشی ها کلینیک (owner_type='clinic') */
public function findByClinic(Clinic $clinic): array
{
return $this->createQueryBuilder('s')
return $this->listWithRelations()
->where('s.clinic = :clinic')
->andWhere('s.ownerType = :type')
->setParameter('clinic', $clinic)
->setParameter('type', DoctorSecretary::OWNER_CLINIC)
->orderBy('s.createdAt', 'DESC')
->getQuery()
->getResult();
}