Contact n+1 issue solved..
This commit is contained in:
parent
1f68dd4b4d
commit
b5d8499ac7
|
|
@ -30,7 +30,20 @@ class Vendors extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
$vendors = Contact::with('media', 'bills.histories', 'bills.totals', 'bills.transactions', 'bills.media')->vendor()->collect();
|
||||
$vendors = Contact::with([
|
||||
'media',
|
||||
'bills.histories',
|
||||
'bills.totals',
|
||||
'bills.transactions',
|
||||
'bills.media'
|
||||
])
|
||||
->withCount([
|
||||
'contact_persons as contact_persons_with_email_count' => function ($query) {
|
||||
$query->whereNotNull('email');
|
||||
}
|
||||
])
|
||||
->vendor()
|
||||
->collect();
|
||||
|
||||
return $this->response('purchases.vendors.index', compact('vendors'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,20 @@ class Customers extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
$customers = Contact::customer()->with('media', 'invoices.histories', 'invoices.totals', 'invoices.transactions', 'invoices.media')->collect();
|
||||
$customers = Contact::customer()
|
||||
->with([
|
||||
'media',
|
||||
'invoices.histories',
|
||||
'invoices.totals',
|
||||
'invoices.transactions',
|
||||
'invoices.media'
|
||||
])
|
||||
->withCount([
|
||||
'contact_persons as contact_persons_with_email_count' => function ($query) {
|
||||
$query->whereNotNull('email');
|
||||
}
|
||||
])
|
||||
->collect();
|
||||
|
||||
return $this->response('sales.customers.index', compact('customers'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,7 +303,11 @@ class Contact extends Model
|
|||
{
|
||||
if (! empty($this->email)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->contact_persons_with_email_count)) {
|
||||
return $this->contact_persons_with_email_count > 0;
|
||||
}
|
||||
|
||||
if ($this->contact_persons()->whereNotNull('email')->count()) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -274,14 +274,22 @@ class Document extends Model
|
|||
|
||||
public function getSentAtAttribute(string $value = null)
|
||||
{
|
||||
$sent = $this->histories()->where('document_histories.status', 'sent')->first();
|
||||
if ($this->relationLoaded('histories')) {
|
||||
$sent = $this->histories->where('status', 'sent')->first();
|
||||
} else {
|
||||
$sent = $this->histories()->where('document_histories.status', 'sent')->first();
|
||||
}
|
||||
|
||||
return $sent->created_at ?? null;
|
||||
}
|
||||
|
||||
public function getReceivedAtAttribute(string $value = null)
|
||||
{
|
||||
$received = $this->histories()->where('document_histories.status', 'received')->first();
|
||||
if ($this->relationLoaded('histories')) {
|
||||
$received = $this->histories->where('status', 'received')->first();
|
||||
} else {
|
||||
$received = $this->histories()->where('document_histories.status', 'received')->first();
|
||||
}
|
||||
|
||||
return $received->created_at ?? null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Performance;
|
||||
|
||||
use App\Jobs\Common\CreateContact;
|
||||
use App\Models\Common\Contact;
|
||||
use App\Models\Common\ContactPerson;
|
||||
use App\Models\Document\Document;
|
||||
use App\Models\Document\DocumentHistory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
/**
|
||||
* Test class to validate Vendor N+1 query optimizations
|
||||
* Ensures contact_persons and document_histories N+1 fixes work correctly
|
||||
*/
|
||||
class VendorN1QueryTest extends FeatureTestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Test contact_persons withCount optimization prevents N+1
|
||||
*/
|
||||
public function testContactPersonsWithCountOptimization()
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
// Create vendors using factory
|
||||
$vendors = Contact::factory()->vendor()->count(10)->create([
|
||||
'company_id' => $this->company->id
|
||||
]);
|
||||
|
||||
// Add contact persons to vendors (with required type field)
|
||||
foreach ($vendors as $vendor) {
|
||||
for ($j = 0; $j < rand(1, 3); $j++) {
|
||||
ContactPerson::create([
|
||||
'company_id' => $this->company->id,
|
||||
'type' => 'vendor', // Required field
|
||||
'contact_id' => $vendor->id,
|
||||
'name' => 'Person ' . $j,
|
||||
'email' => 'person' . $vendor->id . '_' . $j . '@example.com',
|
||||
'phone' => '123-456-7890'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test optimized query with withCount
|
||||
DB::enableQueryLog();
|
||||
|
||||
$optimizedVendors = Contact::vendor()
|
||||
->withCount([
|
||||
'contact_persons as contact_persons_with_email_count' => function ($query) {
|
||||
$query->whereNotNull('email');
|
||||
}
|
||||
])
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
// Access has_email attribute (uses cached count)
|
||||
foreach ($optimizedVendors as $vendor) {
|
||||
$hasEmail = $vendor->has_email;
|
||||
}
|
||||
|
||||
$queryCount = count(DB::getQueryLog());
|
||||
DB::disableQueryLog();
|
||||
|
||||
// Assertions
|
||||
$this->assertEquals(10, $optimizedVendors->count());
|
||||
$this->assertLessThan(5, $queryCount, 'Should use less than 5 queries');
|
||||
|
||||
// Verify count attribute is loaded
|
||||
foreach ($optimizedVendors as $vendor) {
|
||||
$this->assertTrue(isset($vendor->contact_persons_with_email_count));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test document histories eager loading prevents N+1
|
||||
*/
|
||||
public function testDocumentHistoriesEagerLoadingOptimization()
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
// Create vendor with bills
|
||||
$vendor = Contact::factory()->vendor()->create([
|
||||
'company_id' => $this->company->id
|
||||
]);
|
||||
|
||||
// Create bills with histories
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$bill = Document::factory()->bill()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'contact_id' => $vendor->id
|
||||
]);
|
||||
|
||||
// Add sent and received histories (with required type field)
|
||||
DocumentHistory::create([
|
||||
'company_id' => $this->company->id,
|
||||
'type' => Document::BILL_TYPE, // Required field
|
||||
'document_id' => $bill->id,
|
||||
'status' => 'sent',
|
||||
'notify' => 0,
|
||||
'description' => 'Sent to vendor'
|
||||
]);
|
||||
|
||||
DocumentHistory::create([
|
||||
'company_id' => $this->company->id,
|
||||
'type' => Document::BILL_TYPE, // Required field
|
||||
'document_id' => $bill->id,
|
||||
'status' => 'received',
|
||||
'notify' => 0,
|
||||
'description' => 'Received from vendor'
|
||||
]);
|
||||
}
|
||||
|
||||
// Test optimized query with eager loading
|
||||
DB::enableQueryLog();
|
||||
|
||||
$optimizedBills = Document::bill()
|
||||
->where('contact_id', $vendor->id)
|
||||
->with('histories')
|
||||
->get();
|
||||
|
||||
// Access sent_at and received_at attributes (uses eager loaded data)
|
||||
foreach ($optimizedBills as $bill) {
|
||||
$sentAt = $bill->sent_at;
|
||||
$receivedAt = $bill->received_at;
|
||||
}
|
||||
|
||||
$queryCount = count(DB::getQueryLog());
|
||||
DB::disableQueryLog();
|
||||
|
||||
// Assertions
|
||||
$this->assertEquals(5, $optimizedBills->count());
|
||||
$this->assertLessThan(5, $queryCount, 'Should use less than 5 queries');
|
||||
|
||||
// Verify histories relationship is loaded
|
||||
foreach ($optimizedBills as $bill) {
|
||||
$this->assertTrue($bill->relationLoaded('histories'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test vendors index page uses optimized queries
|
||||
*/
|
||||
public function testVendorsIndexPageOptimization()
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
// Create realistic test data
|
||||
$vendors = Contact::factory()->vendor()->count(10)->create([
|
||||
'company_id' => $this->company->id
|
||||
]);
|
||||
|
||||
foreach ($vendors->take(5) as $vendor) {
|
||||
// Add contact persons (with required type field)
|
||||
ContactPerson::create([
|
||||
'company_id' => $this->company->id,
|
||||
'type' => 'vendor', // Required field
|
||||
'contact_id' => $vendor->id,
|
||||
'name' => 'Test Person',
|
||||
'email' => 'person@example.com',
|
||||
'phone' => '1234567890'
|
||||
]);
|
||||
|
||||
// Add bills with histories
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$bill = Document::factory()->bill()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'contact_id' => $vendor->id
|
||||
]);
|
||||
|
||||
DocumentHistory::create([
|
||||
'company_id' => $this->company->id,
|
||||
'type' => Document::BILL_TYPE, // Required field
|
||||
'document_id' => $bill->id,
|
||||
'status' => 'sent',
|
||||
'notify' => 0,
|
||||
'description' => 'Sent'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test optimized controller query
|
||||
DB::enableQueryLog();
|
||||
|
||||
$optimizedVendors = Contact::with([
|
||||
'media',
|
||||
'bills.histories',
|
||||
'bills.totals',
|
||||
'bills.transactions',
|
||||
'bills.media'
|
||||
])
|
||||
->withCount([
|
||||
'contact_persons as contact_persons_with_email_count' => function ($query) {
|
||||
$query->whereNotNull('email');
|
||||
}
|
||||
])
|
||||
->vendor()
|
||||
->collect();
|
||||
|
||||
// Simulate view access
|
||||
foreach ($optimizedVendors as $vendor) {
|
||||
$hasEmail = $vendor->has_email;
|
||||
$open = $vendor->open;
|
||||
|
||||
foreach ($vendor->bills as $bill) {
|
||||
$sentAt = $bill->sent_at;
|
||||
}
|
||||
}
|
||||
|
||||
$queryCount = count(DB::getQueryLog());
|
||||
DB::disableQueryLog();
|
||||
|
||||
// Assertions
|
||||
$this->assertGreaterThan(0, $optimizedVendors->count());
|
||||
$this->assertLessThan(15, $queryCount, 'Should use less than 15 queries');
|
||||
|
||||
// Verify relationships are loaded
|
||||
foreach ($optimizedVendors as $vendor) {
|
||||
$this->assertTrue(isset($vendor->contact_persons_with_email_count));
|
||||
|
||||
if ($vendor->bills->count() > 0) {
|
||||
$this->assertTrue($vendor->bills->first()->relationLoaded('histories'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test actual HTTP request to vendors index
|
||||
*/
|
||||
public function testVendorsIndexHttpRequest()
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
// Create test data
|
||||
Contact::factory()->vendor()->count(5)->create([
|
||||
'company_id' => $this->company->id
|
||||
]);
|
||||
|
||||
DB::enableQueryLog();
|
||||
|
||||
$response = $this->get(route('vendors.index'));
|
||||
|
||||
$queryCount = count(DB::getQueryLog());
|
||||
DB::disableQueryLog();
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertViewIs('purchases.vendors.index');
|
||||
|
||||
// Should not use excessive queries
|
||||
$this->assertLessThan(30, $queryCount, 'Vendors index should use reasonable number of queries');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue