From 1cccfa1c47073dafcc54a2ca6b9dd8d4c35d1735 Mon Sep 17 00:00:00 2001 From: Edward Hibbert Date: Mon, 13 Feb 2023 15:23:33 +0000 Subject: [PATCH 1/2] Use eager-loading to reduce N+1 queries and speed things up. Also cache Roles. --- app/Group.php | 3 + app/Http/Controllers/GroupController.php | 3 +- app/Http/Controllers/PartyController.php | 7 ++- app/Party.php | 10 +++- app/Role.php | 5 ++ composer.json | 1 + composer.lock | 70 +++++++++++++++++++++++- routes/web.php | 9 +++ 8 files changed, 100 insertions(+), 8 deletions(-) diff --git a/app/Group.php b/app/Group.php index 3e6075833a..696546634e 100644 --- a/app/Group.php +++ b/app/Group.php @@ -18,6 +18,9 @@ class Group extends Model implements Auditable protected $table = 'groups'; protected $primaryKey = 'idgroups'; + // Eager-loading reduces N+1 queries. + protected $with = ['networks']; + /** * The attributes that are mass assignable. * diff --git a/app/Http/Controllers/GroupController.php b/app/Http/Controllers/GroupController.php index f08c86a4a6..19d10aa474 100644 --- a/app/Http/Controllers/GroupController.php +++ b/app/Http/Controllers/GroupController.php @@ -51,7 +51,8 @@ private function indexVariations($tab, $network) // Look for groups we have joined, not just been invited to. We have to explicitly test on deleted_at because // the normal filtering out of soft deletes won't happen for joins. - $your_groups =array_column(Group::join('users_groups', 'users_groups.group', '=', 'groups.idgroups') + $your_groups =array_column(Group::with(['networks']) + ->join('users_groups', 'users_groups.group', '=', 'groups.idgroups') ->leftJoin('events', 'events.group', '=', 'groups.idgroups') ->where('users_groups.user', $user->id) ->where('users_groups.status', 1) diff --git a/app/Http/Controllers/PartyController.php b/app/Http/Controllers/PartyController.php index e572c79373..f76f2a8899 100644 --- a/app/Http/Controllers/PartyController.php +++ b/app/Http/Controllers/PartyController.php @@ -52,7 +52,7 @@ public static function expandEvent($event, $group = null) if (is_null($group)) { // We are showing events for multiple groups and so we need to pass the relevant group, in order that // we can show the group name and link to it. - $thisone['group'] = \App\Group::find($event->group); + $thisone['group'] = \App\Group::with('networks')->find($event->group); } $thisone['attending'] = Auth::user() && $event->isBeingAttendedBy(Auth::user()->id); @@ -132,7 +132,7 @@ public function index($group_id = null) } // ...and any other upcoming approved events - $other_upcoming_events = Party::future()-> + $other_upcoming_events = Party::with('theGroup.networks')->future()-> whereNotIn('idevents', \Illuminate\Support\Arr::pluck($events, 'idevents'))-> get(); @@ -373,7 +373,8 @@ public function edit($id, Request $request) $images = null; } - $allGroups = Group::orderBy('name')->get(); + // Fetch the nextwork here - avoids fetching for each group as we encode. + $allGroups = Group::with('networks')->orderBy('name')->get(); if ($request->isMethod('post') && ! empty($request->post())) { $id = $request->post('id'); diff --git a/app/Party.php b/app/Party.php index eb03e8a96c..9438e9e136 100644 --- a/app/Party.php +++ b/app/Party.php @@ -52,6 +52,9 @@ class Party extends Model implements Auditable ]; protected $hidden = ['created_at', 'deleted_at', 'frequency', 'group', 'group', 'user_id', 'wordpress_post_id', 'cancelled', 'devices_updated_at']; + // Eager-loading the group reduces N+1 queries. + protected $with = 'theGroup'; + // Append data to Model protected $appends = ['participants', 'ShareableLink', 'event_date_local', 'start_local', 'end_local']; @@ -334,9 +337,9 @@ public function scopeForUser($query, $userids = null) { // The queries here are not desperately efficient, but we're battling Eloquent a bit. The data size is // low enough it's not really an issue. $this->defaultUserIds($userids); - $hostFor = Party::hostFor($userids); - $attending = Party::attendingOrAttended($userids); - $memberOf = Party::memberOfGroup($userids); + $hostFor = Party::with('theGroup.networks')->hostFor($userids); + $attending = Party::with('theGroup.networks')->attendingOrAttended($userids); + $memberOf = Party::with('theGroup.networks')->memberOfGroup($userids); // In theory $query could contain something other than all(). return $query->whereIn('idevents', $hostFor-> @@ -385,6 +388,7 @@ public function scopeUpcomingEventsInUserArea($query, $user) return $this ->select(DB::raw('`events`.*, ( 6371 * acos( cos( radians('.$user->latitude.') ) * cos( radians( events.latitude ) ) * cos( radians( events.longitude ) - radians('.$user->longitude.') ) + sin( radians('.$user->latitude.') ) * sin( radians( events.latitude ) ) ) ) AS distance')) ->join('groups', 'groups.idgroups', '=', 'events.group') + ->join('networks', 'networks.id', '=', 'group_network.network_id') ->join('users_groups', 'users_groups.group', '=', 'groups.idgroups') ->where(function ($query) use ($exclude_group_ids) { $query->whereNotIn('events.group', $exclude_group_ids) diff --git a/app/Role.php b/app/Role.php index 2b8f2e0e26..6ee341cad8 100644 --- a/app/Role.php +++ b/app/Role.php @@ -4,6 +4,7 @@ use DB; use Illuminate\Database\Eloquent\Model; +use Rennokki\QueryCache\Traits\QueryCacheable; class Role extends Model { @@ -22,6 +23,10 @@ class Role extends Model protected $table = 'roles'; protected $primaryKey = 'idroles'; + use QueryCacheable; + + protected $cacheFor = 180; // 3 minutes + /** * The attributes that are mass assignable. * diff --git a/composer.json b/composer.json index 9e739e4002..f23cd91ef9 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "mcamara/laravel-localization": "^1.7", "msurguy/honeypot": "^1.1", "owen-it/laravel-auditing": "^12.1", + "rennokki/laravel-eloquent-query-cache": "^3.3", "sentry/sentry-laravel": "^2.11", "soundasleep/html2text": "^1.1", "spatie/calendar-links": "^1.6", diff --git a/composer.lock b/composer.lock index 19cee9cb10..d97904792e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1087038ea78e6372950dde4b5d2a11ca", + "content-hash": "86c42496c9ca09c1222f401c5485c713", "packages": [ { "name": "addwiki/mediawiki-api", @@ -5653,6 +5653,74 @@ ], "time": "2023-01-12T18:13:24+00:00" }, + { + "name": "rennokki/laravel-eloquent-query-cache", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/renoki-co/laravel-eloquent-query-cache.git", + "reference": "b0814fec43dd82f718fd10c4b0a2c1e25e7705fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/renoki-co/laravel-eloquent-query-cache/zipball/b0814fec43dd82f718fd10c4b0a2c1e25e7705fb", + "reference": "b0814fec43dd82f718fd10c4b0a2c1e25e7705fb", + "shasum": "" + }, + "require": { + "illuminate/database": "^8.83|^9.35", + "illuminate/support": "^8.83|^9.35" + }, + "require-dev": { + "chelout/laravel-relationship-events": "^1.5", + "laravel/legacy-factories": "^1.3", + "livewire/livewire": "dev-master", + "mockery/mockery": "^1.5", + "orchestra/database": "^6.28|^7.0", + "orchestra/testbench": "^6.28|^7.0", + "orchestra/testbench-core": "^6.28|^7.0", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Rennokki\\QueryCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alex Renoki", + "email": "alex@renoki.org", + "homepage": "https://github.com/rennokki", + "role": "Developer" + } + ], + "description": "Adding cache on your Laravel Eloquent queries' results is now a breeze.", + "homepage": "https://github.com/renoki-co/laravel-eloquent-query-cache", + "keywords": [ + "caching", + "eloquent", + "laravel", + "query", + "remember", + "sql" + ], + "support": { + "issues": "https://github.com/renoki-co/laravel-eloquent-query-cache/issues", + "source": "https://github.com/renoki-co/laravel-eloquent-query-cache/tree/3.3.1" + }, + "funding": [ + { + "url": "https://github.com/rennokki", + "type": "github" + } + ], + "time": "2022-11-13T18:49:44+00:00" + }, { "name": "sentry/sdk", "version": "3.3.0", diff --git a/routes/web.php b/routes/web.php index 8cd6ec07b4..b84cbe6850 100644 --- a/routes/web.php +++ b/routes/web.php @@ -449,3 +449,12 @@ Route::get('/find', [StyleController::class, 'find']); }); }); + +// Useful code to log all queries. This is particularly useful when trying to reduce the number of queries; if +// Laravel debug is turned on then the Queries tab on the client shows them briefly and then gets reset. That's +// long enough to spot pages with too many queries, but not long enough to see what they are. +//\DB::listen(function($sql) { +// \Log::info($sql->sql); +// \Log::info($sql->bindings); +// \Log::info($sql->time); +//}); \ No newline at end of file From a7f7974c1e190c35dccebff2f4a8eda9a24d182f Mon Sep 17 00:00:00 2001 From: Edward Hibbert Date: Mon, 13 Feb 2023 15:56:44 +0000 Subject: [PATCH 2/2] Test fixes. --- app/Party.php | 1 + tests/Feature/Admin/Users/ViewUsersTest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Party.php b/app/Party.php index 9438e9e136..eb2cc54164 100644 --- a/app/Party.php +++ b/app/Party.php @@ -388,6 +388,7 @@ public function scopeUpcomingEventsInUserArea($query, $user) return $this ->select(DB::raw('`events`.*, ( 6371 * acos( cos( radians('.$user->latitude.') ) * cos( radians( events.latitude ) ) * cos( radians( events.longitude ) - radians('.$user->longitude.') ) + sin( radians('.$user->latitude.') ) * sin( radians( events.latitude ) ) ) ) AS distance')) ->join('groups', 'groups.idgroups', '=', 'events.group') + ->join('group_network', 'groups.idgroups', '=', 'group_network.group_id') ->join('networks', 'networks.id', '=', 'group_network.network_id') ->join('users_groups', 'users_groups.group', '=', 'groups.idgroups') ->where(function ($query) use ($exclude_group_ids) { diff --git a/tests/Feature/Admin/Users/ViewUsersTest.php b/tests/Feature/Admin/Users/ViewUsersTest.php index b1a08b3004..1647eb214a 100644 --- a/tests/Feature/Admin/Users/ViewUsersTest.php +++ b/tests/Feature/Admin/Users/ViewUsersTest.php @@ -32,7 +32,7 @@ public function an_admin_can_view_list_of_users() $response = $this->get('/user/all'); // Then the users should be in the list - $response->assertSeeText(e($users[0]->name)); + $response->assertSeeText($users[0]->name); } /** @test */