Skip to content

Commit

Permalink
refactor: normalize forum aggregates
Browse files Browse the repository at this point in the history
Even though the queries were ultra efficient before (always less than 5ms),
the total combined cpu execution time for forums is not greater than 0.3%
of all total execution time. Opt for cleaner code and a normalized database
instead.

In a forum with 40k posts and 5k topics:

Topic index: 26 ms -> 83ms.
The forum category topic index: 29 ms -> 63 ms.
The forum index: 20 ms -> 55 ms.
The forum topic index: 19 ms -> 25 ms.
User topics: 18 ms -> 21 ms.
Subscriptions: 19 ms -> 20 ms.

These numbers are very reasonable and normalization should be fine here.

Unfortunately, Laravel doesn't have a function for hasManyThrough()->latestOfMany() so the eager load has to be done manually.
  • Loading branch information
Roardom committed Oct 30, 2024
1 parent 1dfb43b commit f0ce785
Show file tree
Hide file tree
Showing 22 changed files with 191 additions and 287 deletions.
4 changes: 0 additions & 4 deletions app/Console/Commands/AutoSoftDeleteDisabledUsers.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,6 @@ final public function handle(): void
'first_post_user_id' => User::SYSTEM_USER_ID,
]);

Topic::where('last_post_user_id', '=', $user->id)->update([
'last_post_user_id' => User::SYSTEM_USER_ID,
]);

PrivateMessage::where('sender_id', '=', $user->id)->update([
'sender_id' => User::SYSTEM_USER_ID,
]);
Expand Down
41 changes: 31 additions & 10 deletions app/Http/Controllers/ForumController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,39 @@ class ForumController extends Controller
/**
* Show All Forums.
*/
public function index(Request $request): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
public function index(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
$categories = ForumCategory::query()
->with([
'forums' => fn ($query) => $query->authorized(canReadTopic: true)->orderBy('position')->withCount('topics', 'posts'),
])
->orderBy('position')
->get()
->filter(fn ($category) => $category->forums->isNotEmpty());

$latestPosts = Post::query()
->with([
'user' => fn ($query) => $query->withTrashed(),
'topic',
])
->joinSub(
Post::query()
->selectRaw('MAX(posts.id) AS id, forum_id')
->join('topics', 'posts.topic_id', '=', 'topics.id')
->groupBy('forum_id'),
'latest_posts',
fn ($join) => $join->on('posts.id', '=', 'latest_posts.id')
)
->get();

$categories->transform(function ($category) use ($latestPosts) {
$category->forums->transform(fn ($forum) => $forum->setRelation('latestPost', $latestPosts->firstWhere('forum_id', '=', $forum->id)));

return $category;
});

return view('forum.index', [
'categories' => ForumCategory::query()
->with([
'forums' => fn ($query) => $query->authorized(canReadTopic: true)->orderBy('position'),
'forums.latestPoster' => fn ($query) => $query->withTrashed(),
'forums.lastRepliedTopic',
])
->orderBy('position')
->get()
->filter(fn ($category) => $category->forums->isNotEmpty()),
'categories' => $categories,
'num_posts' => Post::count(),
'num_forums' => Forum::count(),
'num_topics' => Topic::count(),
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public function index(Request $request): \Illuminate\Contracts\View\Factory|\Ill
),
'articles' => $articles,
'topics' => Topic::query()
->with(['user', 'user.group', 'latestPoster', 'reads' => fn ($query) => $query->whereBelongsto($user)])
->with(['user', 'user.group', 'latestPost.user', 'reads' => fn ($query) => $query->whereBelongsto($user)])
->withCount('posts')
->authorized(canReadTopic: true)
->latest()
->take(5)
Expand Down
48 changes: 2 additions & 46 deletions app/Http/Controllers/PostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,6 @@ public function store(Request $request): \Illuminate\Http\RedirectResponse
'topic_id' => $topic->id,
]);

$topic->update([
'last_post_id' => $post->id,
'last_post_user_id' => $user->id,
'num_post' => $topic->posts()->count(),
'last_post_created_at' => $post->created_at,
]);

$forum->update([
'num_post' => $forum->posts()->count(),
'num_topic' => $forum->topics()->count(),
'last_post_user_id' => $user->id,
'last_post_id' => $post->id,
'last_topic_id' => $topic->id,
'last_post_created_at' => $post->created_at,
]);

// Post To Chatbox and Notify Subscribers
$appUrl = config('app.url');
$postUrl = \sprintf('%s/forums/topics/%s/posts/%s', $appUrl, $topic->id, $post->id);
Expand Down Expand Up @@ -230,38 +214,10 @@ public function destroy(Request $request, int $id): \Illuminate\Http\RedirectRes

$post->delete();

$latestPost = $topic->latestPostSlow;
$isTopicDeleted = false;

if ($latestPost === null) {
if ($topic->latestPost()->doesntExist()) {
$topic->delete();
$isTopicDeleted = true;
} else {
$latestPoster = $latestPost->user;
$topic->update([
'last_post_id' => $latestPost->id,
'last_post_user_id' => $latestPoster->id,
'num_post' => $topic->posts()->count(),
'last_post_created_at' => $latestPost->created_at,
]);
}

$forum = $topic->forum;
$lastRepliedTopic = $forum->lastRepliedTopicSlow;
$latestPost = $lastRepliedTopic->latestPostSlow;
$latestPoster = $latestPost->user;

$forum->update([
'num_post' => $forum->posts()->count(),
'num_topic' => $forum->topics()->count(),
'last_post_id' => $latestPost->id,
'last_post_user_id' => $latestPoster->id,
'last_topic_id' => $lastRepliedTopic->id,
'last_post_created_at' => $latestPost->created_at,
]);

if ($isTopicDeleted === true) {
return to_route('forums.show', ['id' => $forum->id])
return to_route('forums.show', ['id' => $post->topic->forum_id])
->withSuccess(trans('forum.delete-post-success'));
}

Expand Down
4 changes: 0 additions & 4 deletions app/Http/Controllers/Staff/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,6 @@ protected function destroy(Request $request, User $user): \Illuminate\Http\Redir
'first_post_user_id' => User::SYSTEM_USER_ID,
]);

Topic::where('last_post_user_id', '=', $user->id)->update([
'last_post_user_id' => User::SYSTEM_USER_ID,
]);

PrivateMessage::where('sender_id', '=', $user->id)->update([
'sender_id' => User::SYSTEM_USER_ID,
]);
Expand Down
75 changes: 2 additions & 73 deletions app/Http/Controllers/TopicController.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ public function show(Request $request, int $id): \Illuminate\Contracts\View\Fact
*/
public function create(Request $request, int $id): \Illuminate\Contracts\View\Factory|\Illuminate\View\View|\Illuminate\Http\RedirectResponse
{
$user = $request->user();

$forum = Forum::with('category')->authorized(canStartTopic: true)->findOrFail($id);

return view('forum.forum_topic.create', [
Expand All @@ -109,33 +107,17 @@ public function store(Request $request, int $id): \Illuminate\Http\RedirectRespo
'name' => $request->title,
'state' => 'open',
'first_post_user_id' => $user->id,
'last_post_user_id' => $user->id,
'views' => 0,
'priority' => 0,
'forum_id' => $forum->id,
'num_post' => 1,
]);

$post = Post::create([
Post::create([
'content' => $request->input('content'),
'user_id' => $user->id,
'topic_id' => $topic->id,
]);

$forum->update([
'num_topic' => $forum->topics()->count(),
'num_post' => $forum->posts()->count(),
'last_topic_id' => $topic->id,
'last_post_id' => $post->id,
'last_post_user_id' => $user->id,
'last_post_created_at' => $post->created_at,
]);

$topic->update([
'last_post_id' => $post->id,
'last_post_created_at' => $post->created_at,
]);

// Post To ShoutBox
$appUrl = config('app.url');
$topicUrl = \sprintf('%s/forums/topics/%s', $appUrl, $topic->id);
Expand Down Expand Up @@ -232,50 +214,11 @@ public function update(Request $request, int $id): \Illuminate\Http\RedirectResp

$newForum = Forum::authorized(canStartTopic: true)->whereKey($request->forum_id)->sole();

$oldForum = $topic->forum;

$topic->update([
'name' => $request->name,
'forum_id' => $newForum->id,
]);

if ($oldForum->id === $newForum->id) {
$lastRepliedTopic = $newForum->lastRepliedTopicSlow;

if ($lastRepliedTopic->id === $newForum->last_topic_id) {
$latestPost = $lastRepliedTopic->latestPostSlow;

$newForum->updated_at = $latestPost->created_at;
$newForum->save();
}
} else {
$lastRepliedTopic = $oldForum->lastRepliedTopicSlow;
$latestPost = $lastRepliedTopic->latestPostSlow;
$latestPoster = $latestPost->user;

$oldForum->update([
'num_topic' => $oldForum->topics()->count(),
'num_post' => $oldForum->posts()->count(),
'last_topic_id' => $lastRepliedTopic->id,
'last_post_id' => $latestPost->id,
'last_post_user_id' => $latestPoster->id,
'last_post_created_at' => $latestPost->created_at,
]);

$lastRepliedTopic = $newForum->lastRepliedTopicSlow;
$latestPost = $lastRepliedTopic->latestPostSlow;
$latestPoster = $latestPost->user;

$newForum->update([
'num_topic' => $newForum->topics()->count(),
'num_post' => $newForum->posts()->count(),
'last_topic_id' => $lastRepliedTopic->id,
'last_post_id' => $latestPost->id,
'last_post_user_id' => $latestPoster->id,
'last_post_created_at' => $latestPost->created_at,
]);
}

return to_route('topics.show', ['id' => $topic->id])
->withSuccess('Topic Successfully Edited');
}
Expand All @@ -292,21 +235,7 @@ public function destroy(int $id): \Illuminate\Http\RedirectResponse
$topic->posts()->delete();
$topic->delete();

$forum = $topic->forum;
$lastRepliedTopic = $forum->lastRepliedTopicSlow;
$latestPost = $lastRepliedTopic->latestPostSlow;
$latestPoster = $latestPost->user;

$topic->forum()->update([
'num_topic' => $forum->topics()->count(),
'num_post' => $forum->posts()->count(),
'last_topic_id' => $lastRepliedTopic->id,
'last_post_id' => $latestPost->id,
'last_post_user_id' => $latestPoster->id,
'last_post_created_at' => $latestPost->created_at,
]);

return to_route('forums.show', ['id' => $forum->id])
return to_route('forums.show', ['id' => $topic->forum_id])
->withSuccess('This Topic Is Now Deleted!');
}

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/User/TopicController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ public function index(User $user): \Illuminate\Contracts\View\Factory|\Illuminat
'topics' => $user->topics()
->with([
'user.group',
'latestPoster',
'latestPost.user',
'forum:id,name',
'reads' => fn ($query) => $query->whereBelongsTo(auth()->user()),
])
->withCount('posts')
->authorized(canReadTopic: true)
->latest()
->paginate(25),
Expand Down
10 changes: 8 additions & 2 deletions app/Http/Livewire/ForumCategoryTopicSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace App\Http\Livewire;

use App\Models\ForumCategory;
use App\Models\Post;
use App\Models\Topic;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Url;
Expand Down Expand Up @@ -72,10 +73,11 @@ final public function topics(): \Illuminate\Pagination\LengthAwarePaginator
->select('topics.*')
->with([
'user.group',
'latestPoster',
'latestPost.user',
'forum',
'reads' => fn ($query) => $query->whereBelongsto(auth()->user()),
])
->withCount('posts')
->whereRelation('forum', 'forum_category_id', '=', $this->category->id)
->authorized(canReadTopic: true)
->when($this->search !== '', fn ($query) => $query->where('name', 'LIKE', '%'.$this->search.'%'))
Expand Down Expand Up @@ -117,7 +119,11 @@ final public function topics(): \Illuminate\Pagination\LengthAwarePaginator
->whereDoesntHave('reads', fn ($query) => $query->whereBelongsTo(auth()->user()))
)
->orderByDesc('priority')
->orderBy($this->sortField, $this->sortDirection)
->when(
$this->sortField === 'last_post_created_at',
fn ($query) => $query->orderBy(Post::query()->selectRaw('MAX(id)')->whereColumn('topics.id', '=', 'posts.topic_id'), $this->sortDirection),
fn ($query) => $query->orderBy('created_at', $this->sortDirection),
)
->paginate(25);
}

Expand Down
10 changes: 8 additions & 2 deletions app/Http/Livewire/ForumTopicSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace App\Http\Livewire;

use App\Models\Forum;
use App\Models\Post;
use App\Models\Subscription;
use App\Models\Topic;
use Livewire\Attributes\Computed;
Expand Down Expand Up @@ -77,10 +78,11 @@ final public function topics(): \Illuminate\Pagination\LengthAwarePaginator
->select('topics.*')
->with([
'user.group',
'latestPoster',
'latestPost.user',
'forum:id,name',
'reads' => fn ($query) => $query->whereBelongsto(auth()->user()),
])
->withCount('posts')
->where('topics.forum_id', '=', $this->forum->id)
->authorized(canReadTopic: true)
->when($this->search !== '', fn ($query) => $query->where('name', 'LIKE', '%'.$this->search.'%'))
Expand Down Expand Up @@ -122,7 +124,11 @@ final public function topics(): \Illuminate\Pagination\LengthAwarePaginator
->whereDoesntHave('reads', fn ($query) => $query->whereBelongsTo(auth()->user()))
)
->orderByDesc('priority')
->orderBy($this->sortField, $this->sortDirection)
->when(
$this->sortField === 'last_post_created_at',
fn ($query) => $query->orderBy(Post::query()->selectRaw('MAX(id)')->whereColumn('topics.id', '=', 'posts.topic_id'), $this->sortDirection),
fn ($query) => $query->orderBy('created_at', $this->sortDirection),
)
->paginate(25);
}

Expand Down
25 changes: 23 additions & 2 deletions app/Http/Livewire/SubscribedForum.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace App\Http\Livewire;

use App\Models\Forum;
use App\Models\Post;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
Expand All @@ -31,12 +32,32 @@ class SubscribedForum extends Component
#[Computed]
final public function forums()
{
return Forum::query()
->with('latestPoster', 'lastRepliedTopic')
$forums = Forum::query()
->withCount('topics', 'posts')
->whereRelation('subscribedUsers', 'users.id', '=', auth()->id())
->authorized(canReadTopic: true)
->orderBy('position')
->paginate(25, ['*'], 'subscribedForumsPage');

$latestPosts = Post::query()
->with([
'user' => fn ($query) => $query->withTrashed(),
'topic',
])
->joinSub(
Post::query()
->selectRaw('MAX(posts.id) AS id, forum_id')
->join('topics', 'posts.topic_id', '=', 'topics.id')
->whereIntegerInRaw('forum_id', $forums->pluck('id'))
->groupBy('forum_id'),
'latest_posts',
fn ($join) => $join->on('posts.id', '=', 'latest_posts.id')
)
->get();

$forums->transform(fn ($forum) => $forum->setRelation('latestPost', $latestPosts->firstWhere('forum_id', '=', $forum->id)));

return $forums;
}

final public function updatedSubscribedForumsPage(): void
Expand Down
Loading

0 comments on commit f0ce785

Please sign in to comment.