refactor n+1 query optimization

This commit is contained in:
Cihan Şentürk 2025-07-21 15:03:52 +03:00 committed by GitHub
parent 355c34920c
commit 139be97e11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 39 additions and 183 deletions

View File

@ -44,8 +44,19 @@ class Invoices extends Controller
*/ */
public function show(Document $invoice, Request $request) public function show(Document $invoice, Request $request)
{ {
// Use DocumentService to optimally load all relationships needed for template rendering $invoice->load([
app(\App\Services\DocumentService::class)->loadForShow($invoice); 'items.taxes.tax',
'items.item',
'totals',
'contact',
'currency',
'category',
'histories',
'media',
'transactions',
'recurring',
'children',
]);
$payment_methods = Modules::getPaymentMethods(); $payment_methods = Modules::getPaymentMethods();

View File

@ -45,8 +45,19 @@ class Bills extends Controller
*/ */
public function show(Document $bill) public function show(Document $bill)
{ {
// Use DocumentService to optimally load all relationships needed for template rendering $bill->load([
app(\App\Services\DocumentService::class)->loadForShow($bill); 'items.taxes.tax',
'items.item',
'totals',
'contact',
'currency',
'category',
'histories',
'media',
'transactions',
'recurring',
'children',
]);
return view('purchases.bills.show', compact('bill')); return view('purchases.bills.show', compact('bill'));
} }

View File

@ -47,8 +47,19 @@ class Invoices extends Controller
*/ */
public function show(Document $invoice) public function show(Document $invoice)
{ {
// Use DocumentService to optimally load all relationships needed for template rendering $invoice->load([
app(\App\Services\DocumentService::class)->loadForShow($invoice); 'items.taxes.tax',
'items.item',
'totals',
'contact',
'currency',
'category',
'histories',
'media',
'transactions',
'recurring',
'children',
]);
return view('sales.invoices.show', compact('invoice')); return view('sales.invoices.show', compact('invoice'));
} }

View File

@ -1,117 +0,0 @@
<?php
namespace App\Services;
use App\Models\Document\Document;
/**
* DocumentService provides optimized document loading with all necessary relationships
* to prevent N+1 queries in templates and views.
*/
class DocumentService
{
/**
* Standard relationships needed for document template rendering
* These relationships are optimized to prevent N+1 queries
*/
private const TEMPLATE_RELATIONSHIPS = [
'items.taxes.tax', // Document items with tax calculations
'items.item', // Item details for templates
'totals', // Document totals for display
'contact', // Contact information
'currency', // Currency for formatting
'category', // Document category
'histories', // Document history for audit trail
'media' // Attached files/images
];
/**
* Additional relationships for show pages
*/
private const SHOW_RELATIONSHIPS = [
'transactions', // Payment/transaction history
'recurring', // Recurring document settings
'children' // Child documents (recurring)
];
/**
* Load document with all relationships needed for template rendering
* Optimized to prevent N+1 queries in document templates
*
* @param Document $document
* @param array $additionalRelationships
* @return Document
*/
public function loadForTemplate(Document $document, array $additionalRelationships = []): Document
{
$relationships = array_merge(self::TEMPLATE_RELATIONSHIPS, $additionalRelationships);
return $document->load($relationships);
}
/**
* Load document with all relationships needed for show pages
* Includes template relationships plus show-specific ones
*
* @param Document $document
* @param array $additionalRelationships
* @return Document
*/
public function loadForShow(Document $document, array $additionalRelationships = []): Document
{
$relationships = array_merge(
self::TEMPLATE_RELATIONSHIPS,
self::SHOW_RELATIONSHIPS,
$additionalRelationships
);
return $document->load($relationships);
}
/**
* Load minimal relationships for document listing
* Optimized for index pages with many documents
*
* @param Document $document
* @return Document
*/
public function loadForIndex(Document $document): Document
{
return $document->load([
'contact',
'category',
'currency',
'last_history'
]);
}
/**
* Check if document has all necessary relationships loaded
* Useful for debugging and ensuring optimal performance
*
* @param Document $document
* @return bool
*/
public function hasTemplateRelationshipsLoaded(Document $document): bool
{
foreach (self::TEMPLATE_RELATIONSHIPS as $relationship) {
if (!$document->relationLoaded($this->getBaseRelationship($relationship))) {
return false;
}
}
return true;
}
/**
* Get the base relationship name from a nested relationship string
* e.g., "items.taxes.tax" returns "items"
*
* @param string $relationship
* @return string
*/
private function getBaseRelationship(string $relationship): string
{
return explode('.', $relationship)[0];
}
}

View File

@ -6,7 +6,6 @@ use App\Models\Common\Dashboard;
use App\Models\Common\Item; use App\Models\Common\Item;
use App\Models\Document\Document; use App\Models\Document\Document;
use App\Models\Setting\Tax; use App\Models\Setting\Tax;
use App\Services\DocumentService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Tests\Feature\FeatureTestCase; use Tests\Feature\FeatureTestCase;
@ -102,65 +101,6 @@ class N1QueryOptimizationTest extends FeatureTestCase
$this->assertLessThan(10, $queryCount, 'Autocomplete should not use excessive queries'); $this->assertLessThan(10, $queryCount, 'Autocomplete should not use excessive queries');
} }
/**
* Test DocumentService optimization
* This validates our DocumentService implementation
*/
public function testDocumentServiceOptimization()
{
$this->loginAs();
// Create a realistic document scenario
$document = Document::factory()->invoice()->create([
'company_id' => $this->company->id
]);
$item = Item::factory()->create(['company_id' => $this->company->id]);
$tax = Tax::factory()->create(['company_id' => $this->company->id]);
$documentItem = $document->items()->create([
'company_id' => $this->company->id,
'type' => $document->type,
'item_id' => $item->id,
'name' => $item->name,
'quantity' => 1,
'price' => 100,
'total' => 100
]);
$documentItem->taxes()->create([
'company_id' => $this->company->id,
'type' => $document->type,
'document_id' => $document->id,
'tax_id' => $tax->id,
'name' => $tax->name,
'amount' => 10
]);
// Test our DocumentService
$documentService = app(DocumentService::class);
// Test loadForShow method
$freshDocument = Document::find($document->id);
$optimizedDocument = $documentService->loadForShow($freshDocument);
// Verify all critical relationships are loaded
$this->assertTrue($optimizedDocument->relationLoaded('items'), 'Items should be loaded');
$this->assertTrue($optimizedDocument->relationLoaded('contact'), 'Contact should be loaded');
$this->assertTrue($optimizedDocument->relationLoaded('currency'), 'Currency should be loaded');
$this->assertTrue($optimizedDocument->relationLoaded('totals'), 'Totals should be loaded');
// Test nested relationships
if ($optimizedDocument->items->count() > 0) {
$firstItem = $optimizedDocument->items->first();
$this->assertTrue($firstItem->relationLoaded('taxes'), 'Item taxes should be loaded');
$this->assertTrue($firstItem->relationLoaded('item'), 'Item details should be loaded');
}
// Test service methods
$this->assertTrue($documentService->hasTemplateRelationshipsLoaded($optimizedDocument));
}
/** /**
* Test that our optimizations prevent the classic N+1 scenario * Test that our optimizations prevent the classic N+1 scenario
* This validates that eager loading works correctly * This validates that eager loading works correctly