Fix perf with cache
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
|
||||||
namespace Managing.Application.Abstractions.Repositories;
|
namespace Managing.Application.Abstractions.Repositories;
|
||||||
|
|
||||||
@@ -11,4 +12,5 @@ public interface IAccountRepository
|
|||||||
Task UpdateAccountAsync(Account account);
|
Task UpdateAccountAsync(Account account);
|
||||||
void DeleteAccountByName(string name);
|
void DeleteAccountByName(string name);
|
||||||
Task<IEnumerable<Account>> GetAccountsAsync();
|
Task<IEnumerable<Account>> GetAccountsAsync();
|
||||||
|
Task<IEnumerable<Account>> GetAccountsByUserAsync(User user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Managing.Application.Abstractions.Repositories;
|
|||||||
public interface IUserRepository
|
public interface IUserRepository
|
||||||
{
|
{
|
||||||
Task<User> GetUserByAgentNameAsync(string agentName);
|
Task<User> GetUserByAgentNameAsync(string agentName);
|
||||||
Task<User> GetUserByNameAsync(string name);
|
Task<User> GetUserByNameAsync(string name, bool fetchAccounts = false);
|
||||||
Task<IEnumerable<User>> GetAllUsersAsync();
|
Task<IEnumerable<User>> GetAllUsersAsync();
|
||||||
Task SaveOrUpdateUserAsync(User user);
|
Task SaveOrUpdateUserAsync(User user);
|
||||||
}
|
}
|
||||||
@@ -199,10 +199,11 @@ public class AccountService : IAccountService
|
|||||||
|
|
||||||
private async Task<IEnumerable<Account>> GetAccountsAsync(User user, bool hideSecrets, bool getBalance)
|
private async Task<IEnumerable<Account>> GetAccountsAsync(User user, bool hideSecrets, bool getBalance)
|
||||||
{
|
{
|
||||||
var result = await _accountRepository.GetAccountsAsync();
|
// Use the new efficient repository method that queries accounts by user directly
|
||||||
|
var result = await _accountRepository.GetAccountsByUserAsync(user);
|
||||||
var accounts = new List<Account>();
|
var accounts = new List<Account>();
|
||||||
|
|
||||||
foreach (var account in result.Where(a => a.User.Name == user.Name))
|
foreach (var account in result)
|
||||||
{
|
{
|
||||||
await ManagePropertiesAsync(hideSecrets, getBalance, account);
|
await ManagePropertiesAsync(hideSecrets, getBalance, account);
|
||||||
accounts.Add(account);
|
accounts.Add(account);
|
||||||
|
|||||||
@@ -144,12 +144,8 @@ public class UserService : IUserService
|
|||||||
|
|
||||||
// Fetch from database (either cache miss or cache disabled)
|
// Fetch from database (either cache miss or cache disabled)
|
||||||
var account = await _accountService.GetAccountByKey(address, true, false);
|
var account = await _accountService.GetAccountByKey(address, true, false);
|
||||||
var user = await _userRepository.GetUserByNameAsync(account.User.Name);
|
var user = await _userRepository.GetUserByNameAsync(account.User.Name, true);
|
||||||
|
|
||||||
// Use proper async version to avoid DbContext concurrency issues
|
|
||||||
user.Accounts = (await _accountService.GetAccountsByUserAsync(user)).ToList();
|
|
||||||
|
|
||||||
// Save to cache for 5 minutes if caching is enabled (JWT middleware calls this on every request)
|
|
||||||
if (useCache)
|
if (useCache)
|
||||||
{
|
{
|
||||||
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5));
|
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5));
|
||||||
|
|||||||
1474
src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.Designer.cs
generated
Normal file
1474
src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Managing.Infrastructure.Databases.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserAccountsNavigationProperty : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1292,7 +1292,7 @@ namespace Managing.Infrastructure.Databases.Migrations
|
|||||||
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b =>
|
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User")
|
b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User")
|
||||||
.WithMany()
|
.WithMany("Accounts")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.SetNull)
|
.OnDelete(DeleteBehavior.SetNull)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
@@ -1460,6 +1460,11 @@ namespace Managing.Infrastructure.Databases.Migrations
|
|||||||
{
|
{
|
||||||
b.Navigation("ScenarioIndicators");
|
b.Navigation("ScenarioIndicators");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,7 @@ public class UserEntity
|
|||||||
[MaxLength(255)] public string? AgentName { get; set; }
|
[MaxLength(255)] public string? AgentName { get; set; }
|
||||||
public string? AvatarUrl { get; set; }
|
public string? AvatarUrl { get; set; }
|
||||||
public string? TelegramChannel { get; set; }
|
public string? TelegramChannel { get; set; }
|
||||||
|
|
||||||
|
// Navigation properties
|
||||||
|
public virtual ICollection<AccountEntity> Accounts { get; set; } = new List<AccountEntity>();
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ public class ManagingDbContext : DbContext
|
|||||||
|
|
||||||
// Configure relationship with User
|
// Configure relationship with User
|
||||||
entity.HasOne(e => e.User)
|
entity.HasOne(e => e.User)
|
||||||
.WithMany()
|
.WithMany(u => u.Accounts)
|
||||||
.HasForeignKey(e => e.UserId)
|
.HasForeignKey(e => e.UserId)
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Managing.Application.Abstractions.Repositories;
|
using Managing.Application.Abstractions.Repositories;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
|
using Managing.Domain.Users;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Managing.Infrastructure.Databases.PostgreSql;
|
namespace Managing.Infrastructure.Databases.PostgreSql;
|
||||||
@@ -22,6 +23,7 @@ public class PostgreSqlAccountRepository : IAccountRepository
|
|||||||
{
|
{
|
||||||
var accountEntity = _context.Accounts
|
var accountEntity = _context.Accounts
|
||||||
.AsTracking() // Explicitly enable tracking for delete operations
|
.AsTracking() // Explicitly enable tracking for delete operations
|
||||||
|
.Include(a => a.User) // Include user to get user name for cache invalidation
|
||||||
.FirstOrDefault(a => a.Name == name);
|
.FirstOrDefault(a => a.Name == name);
|
||||||
if (accountEntity != null)
|
if (accountEntity != null)
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,13 @@ public class PostgreSqlAccountRepository : IAccountRepository
|
|||||||
_cacheService.RemoveValue(keyCacheKey);
|
_cacheService.RemoveValue(keyCacheKey);
|
||||||
_cacheService.RemoveValue(idCacheKey);
|
_cacheService.RemoveValue(idCacheKey);
|
||||||
_cacheService.RemoveValue("all_accounts");
|
_cacheService.RemoveValue("all_accounts");
|
||||||
|
|
||||||
|
// Invalidate user-specific accounts cache
|
||||||
|
if (accountEntity.User != null)
|
||||||
|
{
|
||||||
|
var userAccountsCacheKey = $"accounts_user_{accountEntity.User.Name}";
|
||||||
|
_cacheService.RemoveValue(userAccountsCacheKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +199,10 @@ public class PostgreSqlAccountRepository : IAccountRepository
|
|||||||
// Invalidate all accounts cache since we added a new account
|
// Invalidate all accounts cache since we added a new account
|
||||||
_cacheService.RemoveValue("all_accounts");
|
_cacheService.RemoveValue("all_accounts");
|
||||||
|
|
||||||
|
// Invalidate user-specific accounts cache
|
||||||
|
var userAccountsCacheKey = $"accounts_user_{account.User.Name}";
|
||||||
|
_cacheService.RemoveValue(userAccountsCacheKey);
|
||||||
|
|
||||||
// Cache the new account for future lookups
|
// Cache the new account for future lookups
|
||||||
var nameCacheKey = $"account_{account.Name}";
|
var nameCacheKey = $"account_{account.Name}";
|
||||||
var keyCacheKey = $"account_key_{account.Key}";
|
var keyCacheKey = $"account_key_{account.Key}";
|
||||||
@@ -200,6 +213,41 @@ public class PostgreSqlAccountRepository : IAccountRepository
|
|||||||
_cacheService.SaveValue(idCacheKey, account, TimeSpan.FromHours(1));
|
_cacheService.SaveValue(idCacheKey, account, TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Account>> GetAccountsByUserAsync(User user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check cache first for accounts by user
|
||||||
|
var cacheKey = $"accounts_user_{user.Name}";
|
||||||
|
var cachedAccounts = _cacheService.GetValue<List<Account>>(cacheKey);
|
||||||
|
if (cachedAccounts != null)
|
||||||
|
{
|
||||||
|
return cachedAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
|
||||||
|
|
||||||
|
// Direct SQL query to fetch accounts by user ID - much more efficient than loading all accounts
|
||||||
|
var accountEntities = await _context.Accounts
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(a => a.User)
|
||||||
|
.Where(a => a.UserId == user.Id)
|
||||||
|
.ToListAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var accounts = PostgreSqlMappers.Map(accountEntities).ToList();
|
||||||
|
|
||||||
|
// Cache accounts by user for 30 minutes since user accounts don't change frequently
|
||||||
|
_cacheService.SaveValue(cacheKey, accounts, TimeSpan.FromMinutes(30));
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpdateAccountAsync(Account account)
|
public async Task UpdateAccountAsync(Account account)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -229,6 +277,13 @@ public class PostgreSqlAccountRepository : IAccountRepository
|
|||||||
_cacheService.RemoveValue(keyCacheKey);
|
_cacheService.RemoveValue(keyCacheKey);
|
||||||
_cacheService.RemoveValue(idCacheKey);
|
_cacheService.RemoveValue(idCacheKey);
|
||||||
_cacheService.RemoveValue("all_accounts");
|
_cacheService.RemoveValue("all_accounts");
|
||||||
|
|
||||||
|
// Invalidate user-specific accounts cache
|
||||||
|
if (account.User != null)
|
||||||
|
{
|
||||||
|
var userAccountsCacheKey = $"accounts_user_{account.User.Name}";
|
||||||
|
_cacheService.RemoveValue(userAccountsCacheKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -129,7 +129,27 @@ public static class PostgreSqlMappers
|
|||||||
AgentName = entity.AgentName,
|
AgentName = entity.AgentName,
|
||||||
AvatarUrl = entity.AvatarUrl,
|
AvatarUrl = entity.AvatarUrl,
|
||||||
TelegramChannel = entity.TelegramChannel,
|
TelegramChannel = entity.TelegramChannel,
|
||||||
Id = entity.Id // Assuming Id is the primary key for UserEntity
|
Id = entity.Id, // Assuming Id is the primary key for UserEntity
|
||||||
|
Accounts = entity.Accounts?.Select(MapAccountWithoutUser).ToList() ?? new List<Account>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to map AccountEntity without User to avoid circular reference
|
||||||
|
private static Account MapAccountWithoutUser(AccountEntity entity)
|
||||||
|
{
|
||||||
|
if (entity == null) return null;
|
||||||
|
|
||||||
|
return new Account
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
Name = entity.Name,
|
||||||
|
Exchange = entity.Exchange,
|
||||||
|
Type = entity.Type,
|
||||||
|
Key = entity.Key,
|
||||||
|
Secret = entity.Secret,
|
||||||
|
User = null, // Don't map User to avoid circular reference
|
||||||
|
Balances = new List<Balance>(), // Empty list for now, balances handled separately if needed
|
||||||
|
IsGmxInitialized = entity.IsGmxInitialized
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Managing.Application.Abstractions.Repositories;
|
using Managing.Application.Abstractions.Repositories;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using Managing.Infrastructure.Databases.PostgreSql.Entities;
|
using Managing.Infrastructure.Databases.PostgreSql.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -66,12 +67,12 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
}, nameof(GetUserByAgentNameAsync), ("agentName", agentName));
|
}, nameof(GetUserByAgentNameAsync), ("agentName", agentName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User> GetUserByNameAsync(string name)
|
public async Task<User> GetUserByNameAsync(string name, bool fetchAccounts = false)
|
||||||
{
|
{
|
||||||
return await ExecuteWithLoggingAsync(async () =>
|
return await ExecuteWithLoggingAsync(async () =>
|
||||||
{
|
{
|
||||||
// Check cache first for frequently accessed users
|
// Check cache first for frequently accessed users
|
||||||
var cacheKey = $"user_name_{name}";
|
var cacheKey = fetchAccounts ? $"user_name_with_accounts_{name}" : $"user_name_{name}";
|
||||||
var cachedUser = _cacheService.GetValue<User>(cacheKey);
|
var cachedUser = _cacheService.GetValue<User>(cacheKey);
|
||||||
if (cachedUser != null)
|
if (cachedUser != null)
|
||||||
{
|
{
|
||||||
@@ -82,6 +83,35 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
{
|
{
|
||||||
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
|
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
|
||||||
|
|
||||||
|
User user;
|
||||||
|
|
||||||
|
if (fetchAccounts)
|
||||||
|
{
|
||||||
|
// Fetch user with accounts in a single query
|
||||||
|
var userEntity = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(u => u.Accounts)
|
||||||
|
.Where(u => u.Name == name)
|
||||||
|
.FirstOrDefaultAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (userEntity == null)
|
||||||
|
throw new InvalidOperationException($"User with name '{name}' not found");
|
||||||
|
|
||||||
|
user = PostgreSqlMappers.Map(userEntity);
|
||||||
|
|
||||||
|
// Map accounts using the existing mapper
|
||||||
|
if (userEntity.Accounts != null)
|
||||||
|
{
|
||||||
|
user.Accounts = userEntity.Accounts.Select(PostgreSqlMappers.Map).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user.Accounts = new List<Account>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Optimized query with explicit SELECT to avoid loading unnecessary data
|
// Optimized query with explicit SELECT to avoid loading unnecessary data
|
||||||
var userEntity = await _context.Users
|
var userEntity = await _context.Users
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -100,10 +130,14 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
if (userEntity == null)
|
if (userEntity == null)
|
||||||
throw new InvalidOperationException($"User with name '{name}' not found");
|
throw new InvalidOperationException($"User with name '{name}' not found");
|
||||||
|
|
||||||
var user = PostgreSqlMappers.Map(userEntity);
|
user = PostgreSqlMappers.Map(userEntity);
|
||||||
|
user.Accounts = new List<Account>(); // Initialize empty list
|
||||||
|
}
|
||||||
|
|
||||||
// Cache user for 5 minutes since user data doesn't change frequently
|
// Cache user for 5 minutes since user data doesn't change frequently
|
||||||
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5));
|
// Use shorter cache time when including accounts since accounts change more frequently
|
||||||
|
var cacheTime = fetchAccounts ? TimeSpan.FromMinutes(2) : TimeSpan.FromMinutes(5);
|
||||||
|
_cacheService.SaveValue(cacheKey, user, cacheTime);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -111,7 +145,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
{
|
{
|
||||||
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
|
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
|
||||||
}
|
}
|
||||||
}, nameof(GetUserByNameAsync), ("name", name));
|
}, nameof(GetUserByNameAsync), ("name", name), ("fetchAccounts", fetchAccounts));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<User>> GetAllUsersAsync()
|
public async Task<IEnumerable<User>> GetAllUsersAsync()
|
||||||
@@ -214,7 +248,9 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
|
|
||||||
// Invalidate cache for updated user - handle both old and new AgentName
|
// Invalidate cache for updated user - handle both old and new AgentName
|
||||||
var nameCacheKey = $"user_name_{user.Name}";
|
var nameCacheKey = $"user_name_{user.Name}";
|
||||||
|
var nameWithAccountsCacheKey = $"user_name_with_accounts_{user.Name}";
|
||||||
_cacheService.RemoveValue(nameCacheKey);
|
_cacheService.RemoveValue(nameCacheKey);
|
||||||
|
_cacheService.RemoveValue(nameWithAccountsCacheKey);
|
||||||
|
|
||||||
// Invalidate old AgentName cache if it existed
|
// Invalidate old AgentName cache if it existed
|
||||||
if (!string.IsNullOrEmpty(oldAgentName))
|
if (!string.IsNullOrEmpty(oldAgentName))
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const useCurrentUser = () => {
|
|||||||
}
|
}
|
||||||
store.setLoading(query.isLoading)
|
store.setLoading(query.isLoading)
|
||||||
store.setError(query.error as Error | null)
|
store.setError(query.error as Error | null)
|
||||||
}, [query.data, query.isLoading, query.error, store])
|
}, [query.data, query.isLoading, query.error])
|
||||||
|
|
||||||
// Return both TanStack Query data and store state for flexibility
|
// Return both TanStack Query data and store state for flexibility
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user