Attempt to fix N+1 issues

This commit is contained in:
Mark Gregory
2026-06-16 20:09:21 +01:00
parent 85b96144c0
commit e81a509007
6 changed files with 67 additions and 22 deletions
@@ -24,6 +24,8 @@ final class TicketControlsController extends Controller
*/ */
public function updateAgent(Request $request, Ticket $ticket, WorkflowNotificationService $workflow): RedirectResponse public function updateAgent(Request $request, Ticket $ticket, WorkflowNotificationService $workflow): RedirectResponse
{ {
$ticket->loadMissing($this->ticketActionRelations());
Gate::authorize('update', $ticket); Gate::authorize('update', $ticket);
$validated = $request->validate([ $validated = $request->validate([
@@ -48,7 +50,7 @@ final class TicketControlsController extends Controller
'last_activity_at' => now(), 'last_activity_at' => now(),
]); ]);
$fresh = $ticket->fresh(['agent.company', 'owner.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; $fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
$workflow->dispatch($fresh, 'ticket.updated', Auth::user()); $workflow->dispatch($fresh, 'ticket.updated', Auth::user());
return redirect() return redirect()
@@ -63,6 +65,8 @@ final class TicketControlsController extends Controller
*/ */
public function updateStatus(Request $request, Ticket $ticket, WorkflowNotificationService $workflow, SlaService $sla): RedirectResponse public function updateStatus(Request $request, Ticket $ticket, WorkflowNotificationService $workflow, SlaService $sla): RedirectResponse
{ {
$ticket->loadMissing($this->ticketActionRelations());
Gate::authorize('update', $ticket); Gate::authorize('update', $ticket);
$validated = $request->validate([ $validated = $request->validate([
@@ -79,7 +83,7 @@ final class TicketControlsController extends Controller
$this->ensureBoardContainsStatus($ticket, (int) $status->id); $this->ensureBoardContainsStatus($ticket, (int) $status->id);
$fresh = $ticket->fresh(['agent.company', 'owner.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; $fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
if ($status->is_closed) { if ($status->is_closed) {
$sla->recordClosure($fresh); $sla->recordClosure($fresh);
@@ -97,6 +101,8 @@ final class TicketControlsController extends Controller
*/ */
public function updatePriority(Request $request, Ticket $ticket, WorkflowNotificationService $workflow): RedirectResponse public function updatePriority(Request $request, Ticket $ticket, WorkflowNotificationService $workflow): RedirectResponse
{ {
$ticket->loadMissing($this->ticketActionRelations());
Gate::authorize('update', $ticket); Gate::authorize('update', $ticket);
$validated = $request->validate([ $validated = $request->validate([
@@ -108,7 +114,7 @@ final class TicketControlsController extends Controller
'last_activity_at' => now(), 'last_activity_at' => now(),
]); ]);
$fresh = $ticket->fresh(['agent.company', 'owner.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; $fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
$workflow->dispatch($fresh, 'ticket.updated', Auth::user()); $workflow->dispatch($fresh, 'ticket.updated', Auth::user());
return redirect() return redirect()
@@ -116,6 +122,25 @@ final class TicketControlsController extends Controller
->with('ticket-action-updated', 'Priority updated to '.($fresh->priority?->name ?? 'selected priority').'.'); ->with('ticket-action-updated', 'Priority updated to '.($fresh->priority?->name ?? 'selected priority').'.');
} }
/**
* Relations needed by policies, redirect messages, and workflow templates.
*
* @return list<string>
*/
private function ticketActionRelations(): array
{
return [
'owner.company',
'agent.company',
'status',
'priority',
'category',
'project',
'team',
'product',
];
}
/** /**
* Apply a direct guarded ticket update without relying on Livewire hydration. * Apply a direct guarded ticket update without relying on Livewire hydration.
* *
+3 -3
View File
@@ -224,7 +224,7 @@ final class TicketEdit extends Component
$columns[] = 'company_id'; $columns[] = 'company_id';
} }
$query = User::query()->select($columns)->orderBy('name'); $query = User::query()->select($columns)->with('company')->orderBy('name');
if ($this->company_id !== null && Schema::hasColumn('users', 'company_id')) { if ($this->company_id !== null && Schema::hasColumn('users', 'company_id')) {
$tenantUsers = (clone $query) $tenantUsers = (clone $query)
@@ -248,8 +248,8 @@ final class TicketEdit extends Component
private function freshTicket(Ticket $ticket): Ticket private function freshTicket(Ticket $ticket): Ticket
{ {
return $ticket->fresh([ return $ticket->fresh([
'owner.company', 'owner',
'agent.company', 'agent',
'status', 'status',
'priority', 'priority',
'category', 'category',
+16 -5
View File
@@ -55,8 +55,7 @@ final class TicketShow extends Component
public function mount(Ticket $ticket): void public function mount(Ticket $ticket): void
{ {
Auth::user()?->loadMissing('company'); $ticket->loadMissing($this->ticketDetailRelations());
$ticket->loadMissing(['owner.company', 'agent.company']);
$this->authorize('view', $ticket); $this->authorize('view', $ticket);
$this->ticket = $this->freshTicket($ticket); $this->ticket = $this->freshTicket($ticket);
@@ -262,6 +261,8 @@ final class TicketShow extends Component
public function render(): View public function render(): View
{ {
$this->ticket = $this->freshTicket($this->ticket);
return view('helpdesk::livewire.ticket-show', [ return view('helpdesk::livewire.ticket-show', [
'statuses' => Status::query()->orderBy('sort_order')->get(), 'statuses' => Status::query()->orderBy('sort_order')->get(),
'priorities' => Priority::query()->orderBy('sort_order')->get(), 'priorities' => Priority::query()->orderBy('sort_order')->get(),
@@ -282,9 +283,14 @@ final class TicketShow extends Component
return (int) $value; return (int) $value;
} }
private function freshTicket(Ticket $ticket): Ticket /**
* Relations required by the ticket detail view and workflow renderers.
*
* @return list<string>
*/
private function ticketDetailRelations(): array
{ {
$ticket = $ticket->fresh([ return [
'owner.company', 'owner.company',
'agent.company', 'agent.company',
'status', 'status',
@@ -300,7 +306,12 @@ final class TicketShow extends Component
'checklistItems', 'checklistItems',
'attachments.user.company', 'attachments.user.company',
'gitCommits.repository', 'gitCommits.repository',
]) ?? $ticket; ];
}
private function freshTicket(Ticket $ticket): Ticket
{
$ticket = $ticket->fresh($this->ticketDetailRelations()) ?? $ticket;
$comments = \Modules\Helpdesk\Models\Comment::query() $comments = \Modules\Helpdesk\Models\Comment::query()
->where('ticket_id', $ticket->id) ->where('ticket_id', $ticket->id)
+15
View File
@@ -24,6 +24,21 @@ final class Ticket extends Model
protected $table = 'helpdesk_tickets'; protected $table = 'helpdesk_tickets';
/**
* Relations that are repeatedly displayed in ticket lists/detail views.
*
* Eager loading these user/company relations prevents the debug-bar N+1
* detector from firing when Blade, policies, or workflow rendering read the
* requester or assigned agent details.
*
* @var list<string>
*/
protected $with = [
'owner.company',
'agent.company',
];
/** @var list<string> */ /** @var list<string> */
protected $fillable = [ protected $fillable = [
'reference', 'company_id', 'user_id', 'agent_id', 'status_id', 'board_id', 'priority_id', 'category_id', 'project_id', 'team_id', 'reference', 'company_id', 'user_id', 'agent_id', 'status_id', 'board_id', 'priority_id', 'category_id', 'project_id', 'team_id',
+4 -10
View File
@@ -193,14 +193,8 @@ versions:
"1.3.10": "1.3.10":
changed: changed:
- Fixed the ticket detail and ticket edit agent options queries by replacing unsupported orWhereKey usage with supported id comparisons. - Fixed ticket detail N+1 warnings by eager-loading requester, agent and company relations on ticket hydration, ticket refresh and ticket control POST actions.
- Eager-loaded requester and agent company relations on the ticket detail/edit views to prevent Debugbar N+1 alerts. - Added default eager loading for Helpdesk ticket requester and assigned-agent company details to prevent repeated lazy user/company queries in detail and list views.
- Kept agent, status and priority persistence on CSRF-protected POST routes with compact action buttons. - Replaced remaining unsupported orWhereKey usage with supported id predicates in ticket assignment option queries.
- Kept agent, status and priority controls as compact CSRF-protected POST forms that persist directly to helpdesk_tickets.
- Kept module.json version in sync with changelog.yaml. - Kept module.json version in sync with changelog.yaml.
"1.3.11":
changed:
- Eager-loaded the current user company, ticket requester company, ticket agent company, comment user company and attachment user company on the ticket detail page to remove the remaining Debugbar N+1 alert.
- Eager-loaded company data for assignable agent dropdown users without changing agent, status or priority persistence behaviour.
- Kept module.json version in sync with changelog.yaml.
+1 -1
View File
@@ -22,5 +22,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
], ],
"files": [], "files": [],
"version": "1.3.11" "version": "1.3.10"
} }