refactor n+1 query optimization
This commit is contained in:
parent
355c34920c
commit
139be97e11
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue