Skip to content

Eager Loading

Eager loading is the primary tool for preventing N+1 query problems in the CPR backend.

The N+1 Problem

php
// BAD: 1 query for patients + N queries for visits (one per patient)
$patients = Patient::all();
foreach ($patients as $patient) {
    echo $patient->visits->count(); // Triggers a query each time
}

// GOOD: 2 queries total (patients + visits)
$patients = Patient::with('visits')->get();
foreach ($patients as $patient) {
    echo $patient->visits->count(); // Already loaded, no query
}

Where Eager Loading is Applied

Repositories

The AbstractFilterableRepository::buildQuery() method is where eager loading happens:

php
// TransactionRepository
protected function buildQuery(?string $search, ?array $filters): Builder
{
    return Transaction::query()
        ->with(['items', 'patient', 'branch', 'paymentMethod'])
        ->when($search, fn ($q) =>
            $q->where('transaction_number', 'like', "%{$search}%")
        );
}

Controllers (Single Record)

When loading a single record with its relations:

php
public function show(int $id)
{
    $transaction = $this->service->find($id);
    // Service loads: Transaction::with(['items', 'patient'])->findOrFail($id)
    return TransactionResource::make($transaction);
}

API Resources (Conditional)

Resources use whenLoaded() to conditionally include relations:

php
class TransactionResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'patient' => PatientResource::make($this->whenLoaded('patient')),
            'items' => TransactionItemResource::collection($this->whenLoaded('items')),
        ];
    }
}

If the relation wasn't eager-loaded, whenLoaded() simply omits it from the response instead of triggering a lazy load.

Common Eager Loading Patterns

Nested Relations

php
// Load queue tickets with their patient visits and patients
QueueTicket::with('visit.patient')->get();

Counting Without Loading

php
// Don't load all users, just count them
Branch::withCount('users')->get();
// Access: $branch->users_count

Constraining Eager Loads

php
// Only load active queue tickets
PatientVisit::with(['queueTickets' => function ($query) {
    $query->where('status', 'waiting');
}])->get();

Guidelines

  1. Always eager load in repositories when you know the relation will be used
  2. Use whenLoaded() in Resources to avoid accidental lazy loading
  3. Use withCount() when you only need counts, not full records
  4. Load nested relations with dot notation (visit.patient)
  5. Don't over-eager-load - Only load what the current endpoint needs

CPR - Clinical Patient Records