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:
@@ -63,9 +63,18 @@ class BillingController extends BaseController
|
||||
$sessionDates = $this->sessionRepo->datesForIds($uniqueSessionIds);
|
||||
$patientInfo = $this->sessionRepo->patientInfoForIds($uniqueSessionIds);
|
||||
|
||||
return array_map(function (Claim $claim) use ($itemDetails, $sessionDates, $patientInfo) {
|
||||
// Batch-fetch insurance names instead of one find() per claim (N+1).
|
||||
$insuranceIds = array_values(array_unique(array_map(fn(Claim $c) => $c->getInsuranceId(), $claims)));
|
||||
$insuranceNames = [];
|
||||
if ($insuranceIds !== []) {
|
||||
foreach ($this->insuranceRepo->findBy(['id' => $insuranceIds]) as $ins) {
|
||||
$insuranceNames[$ins->getId()] = $ins->getName();
|
||||
}
|
||||
}
|
||||
|
||||
return array_map(function (Claim $claim) use ($itemDetails, $sessionDates, $patientInfo, $insuranceNames) {
|
||||
$data = $claim->toArray();
|
||||
$data['insurance_name'] = $this->insuranceRepo->find($claim->getInsuranceId())?->getName();
|
||||
$data['insurance_name'] = $insuranceNames[$claim->getInsuranceId()] ?? null;
|
||||
|
||||
$patientName = null;
|
||||
$patientMobile = null;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user