fix(backend): stop loading a user's entire listening history into memory

GetListeningHistory used AsEnumerable() before DistinctBy/Take, which
forced EF Core to materialize every history row for the user just to
keep the last 15 distinct tracks. Now groups by track and takes the
15 most recently played track ids in SQL, then fetches only those.
This commit is contained in:
2026-07-15 16:12:26 +02:00
parent 135cea6c76
commit 1438d9bf1a
@@ -1,4 +1,5 @@
using Mediator; using Mediator;
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
@@ -12,18 +13,24 @@ namespace Musify.Application.Tracks
{ {
public const int ListSize = 15; public const int ListSize = 15;
public ValueTask<IEnumerable<TrackApplicationResponse>> Handle(GetListeningHistoryQuery query, CancellationToken cancellationToken) public async ValueTask<IEnumerable<TrackApplicationResponse>> Handle(GetListeningHistoryQuery query, CancellationToken cancellationToken)
{ {
var listeningHistory = database.ListeningHistories var recentTrackIds = await database.ListeningHistories
.Where(l => l.UserId == query.UserId) .Where(l => l.UserId == query.UserId)
.OrderByDescending(l => l.ListenedAt) .GroupBy(l => l.TrackId)
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count }) .OrderByDescending(g => g.Max(l => l.ListenedAt))
.AsEnumerable() .Select(g => g.Key)
.DistinctBy(t => t.Track.Id)
.Take(ListSize) .Take(ListSize)
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.ListensCount)); .ToListAsync(cancellationToken);
return ValueTask.FromResult(listeningHistory); var tracksById = await database.Tracks
.Where(t => recentTrackIds.Contains(t.Id))
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count })
.ToDictionaryAsync(t => t.Id, cancellationToken);
return recentTrackIds
.Select(id => tracksById[id])
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.ListensCount));
} }
} }
} }