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
+11 -2
View File
@@ -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;