Perf: Fix N+1 queries in entity services
- CardService.GetAllCardsWithStatsAsync: Use single query with Select projection - AccountService.GetAccountDetailsAsync: Use single query with Select projection - TransactionStatisticsService: Calculate stats at DB level, fix N+1 in GetCardStatsForAccountAsync 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -85,23 +85,17 @@ public class AccountService : IAccountService
|
|||||||
if (account == null)
|
if (account == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Get cards linked to this account
|
// Single query with projection to avoid N+1
|
||||||
var cards = await _db.Cards
|
var cardStats = await _db.Cards
|
||||||
.Where(c => c.AccountId == id)
|
.Where(c => c.AccountId == id)
|
||||||
.OrderBy(c => c.Owner)
|
.OrderBy(c => c.Owner)
|
||||||
.ThenBy(c => c.Last4)
|
.ThenBy(c => c.Last4)
|
||||||
.ToListAsync();
|
.Select(c => new CardWithStats
|
||||||
|
|
||||||
var cardStats = new List<CardWithStats>();
|
|
||||||
foreach (var card in cards)
|
|
||||||
{
|
|
||||||
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
|
|
||||||
cardStats.Add(new CardWithStats
|
|
||||||
{
|
{
|
||||||
Card = card,
|
Card = c,
|
||||||
TransactionCount = transactionCount
|
TransactionCount = c.Transactions.Count
|
||||||
});
|
})
|
||||||
}
|
.ToListAsync();
|
||||||
|
|
||||||
// Get transaction count for this account
|
// Get transaction count for this account
|
||||||
var accountTransactionCount = await _db.Transactions.CountAsync(t => t.AccountId == id);
|
var accountTransactionCount = await _db.Transactions.CountAsync(t => t.AccountId == id);
|
||||||
|
|||||||
@@ -55,26 +55,17 @@ public class CardService : ICardService
|
|||||||
|
|
||||||
public async Task<List<CardWithStats>> GetAllCardsWithStatsAsync()
|
public async Task<List<CardWithStats>> GetAllCardsWithStatsAsync()
|
||||||
{
|
{
|
||||||
var cards = await _db.Cards
|
// Single query with projection to avoid N+1
|
||||||
|
return await _db.Cards
|
||||||
.Include(c => c.Account)
|
.Include(c => c.Account)
|
||||||
.OrderBy(c => c.Owner)
|
.OrderBy(c => c.Owner)
|
||||||
.ThenBy(c => c.Last4)
|
.ThenBy(c => c.Last4)
|
||||||
.ToListAsync();
|
.Select(c => new CardWithStats
|
||||||
|
|
||||||
var cardStats = new List<CardWithStats>();
|
|
||||||
|
|
||||||
foreach (var card in cards)
|
|
||||||
{
|
|
||||||
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
|
|
||||||
|
|
||||||
cardStats.Add(new CardWithStats
|
|
||||||
{
|
{
|
||||||
Card = card,
|
Card = c,
|
||||||
TransactionCount = transactionCount
|
TransactionCount = c.Transactions.Count
|
||||||
});
|
})
|
||||||
}
|
.ToListAsync();
|
||||||
|
|
||||||
return cardStats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DeleteValidationResult> CanDeleteCardAsync(int id)
|
public async Task<DeleteValidationResult> CanDeleteCardAsync(int id)
|
||||||
|
|||||||
@@ -36,15 +36,19 @@ public class TransactionStatisticsService : ITransactionStatisticsService
|
|||||||
|
|
||||||
public async Task<TransactionStats> CalculateStatsAsync(IQueryable<Transaction> query)
|
public async Task<TransactionStats> CalculateStatsAsync(IQueryable<Transaction> query)
|
||||||
{
|
{
|
||||||
var allFilteredTransactions = await query.ToListAsync();
|
// Calculate stats at database level instead of loading all transactions into memory
|
||||||
|
var stats = await query
|
||||||
|
.GroupBy(_ => 1) // Group all into one group to aggregate
|
||||||
|
.Select(g => new TransactionStats
|
||||||
|
{
|
||||||
|
Count = g.Count(),
|
||||||
|
TotalDebits = g.Where(t => t.Amount < 0).Sum(t => t.Amount),
|
||||||
|
TotalCredits = g.Where(t => t.Amount > 0).Sum(t => t.Amount),
|
||||||
|
NetAmount = g.Sum(t => t.Amount)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
return new TransactionStats
|
return stats ?? new TransactionStats();
|
||||||
{
|
|
||||||
Count = allFilteredTransactions.Count,
|
|
||||||
TotalDebits = allFilteredTransactions.Where(t => t.Amount < 0).Sum(t => t.Amount),
|
|
||||||
TotalCredits = allFilteredTransactions.Where(t => t.Amount > 0).Sum(t => t.Amount),
|
|
||||||
NetAmount = allFilteredTransactions.Sum(t => t.Amount)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CategorizationStats> GetCategorizationStatsAsync()
|
public async Task<CategorizationStats> GetCategorizationStatsAsync()
|
||||||
@@ -64,24 +68,17 @@ public class TransactionStatisticsService : ITransactionStatisticsService
|
|||||||
|
|
||||||
public async Task<List<CardStats>> GetCardStatsForAccountAsync(int accountId)
|
public async Task<List<CardStats>> GetCardStatsForAccountAsync(int accountId)
|
||||||
{
|
{
|
||||||
var cards = await _db.Cards
|
// Single query with projection to avoid N+1
|
||||||
|
return await _db.Cards
|
||||||
.Where(c => c.AccountId == accountId)
|
.Where(c => c.AccountId == accountId)
|
||||||
.OrderBy(c => c.Owner)
|
.OrderBy(c => c.Owner)
|
||||||
.ThenBy(c => c.Last4)
|
.ThenBy(c => c.Last4)
|
||||||
.ToListAsync();
|
.Select(c => new CardStats
|
||||||
|
|
||||||
var cardStats = new List<CardStats>();
|
|
||||||
foreach (var card in cards)
|
|
||||||
{
|
|
||||||
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
|
|
||||||
cardStats.Add(new CardStats
|
|
||||||
{
|
{
|
||||||
Card = card,
|
Card = c,
|
||||||
TransactionCount = transactionCount
|
TransactionCount = c.Transactions.Count
|
||||||
});
|
})
|
||||||
}
|
.ToListAsync();
|
||||||
|
|
||||||
return cardStats;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user