From b6b11be33a5dbe731607f675d1037c1963445ab0 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 10 Oct 2025 03:42:57 +0700 Subject: [PATCH] Fix perf with cache --- .../Repositories/IAccountRepository.cs | 2 + .../Repositories/IUserRepository.cs | 2 +- .../Accounts/AccountService.cs | 5 +- src/Managing.Application/Users/UserService.cs | 6 +- ...UserAccountsNavigationProperty.Designer.cs | 1474 +++++++++++++++++ ...02933_AddUserAccountsNavigationProperty.cs | 22 + .../ManagingDbContextModelSnapshot.cs | 7 +- .../PostgreSql/Entities/UserEntity.cs | 3 + .../PostgreSql/ManagingDbContext.cs | 2 +- .../PostgreSql/PostgreSqlAccountRepository.cs | 55 + .../PostgreSql/PostgreSqlMappers.cs | 22 +- .../PostgreSql/PostgreSqlUserRepository.cs | 76 +- .../src/app/store/userStore.tsx | 2 +- 13 files changed, 1646 insertions(+), 32 deletions(-) create mode 100644 src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.Designer.cs create mode 100644 src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.cs diff --git a/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs b/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs index 60c5e8a4..53b75bf0 100644 --- a/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs @@ -1,4 +1,5 @@ using Managing.Domain.Accounts; +using Managing.Domain.Users; namespace Managing.Application.Abstractions.Repositories; @@ -11,4 +12,5 @@ public interface IAccountRepository Task UpdateAccountAsync(Account account); void DeleteAccountByName(string name); Task> GetAccountsAsync(); + Task> GetAccountsByUserAsync(User user); } diff --git a/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs b/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs index a1a88157..87e6929f 100644 --- a/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs @@ -5,7 +5,7 @@ namespace Managing.Application.Abstractions.Repositories; public interface IUserRepository { Task GetUserByAgentNameAsync(string agentName); - Task GetUserByNameAsync(string name); + Task GetUserByNameAsync(string name, bool fetchAccounts = false); Task> GetAllUsersAsync(); Task SaveOrUpdateUserAsync(User user); } \ No newline at end of file diff --git a/src/Managing.Application/Accounts/AccountService.cs b/src/Managing.Application/Accounts/AccountService.cs index 7e11d0ee..05beadb8 100644 --- a/src/Managing.Application/Accounts/AccountService.cs +++ b/src/Managing.Application/Accounts/AccountService.cs @@ -199,10 +199,11 @@ public class AccountService : IAccountService private async Task> 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(); - foreach (var account in result.Where(a => a.User.Name == user.Name)) + foreach (var account in result) { await ManagePropertiesAsync(hideSecrets, getBalance, account); accounts.Add(account); diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs index 23618175..813b25ef 100644 --- a/src/Managing.Application/Users/UserService.cs +++ b/src/Managing.Application/Users/UserService.cs @@ -144,12 +144,8 @@ public class UserService : IUserService // Fetch from database (either cache miss or cache disabled) 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) { _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5)); diff --git a/src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.Designer.cs new file mode 100644 index 00000000..03acfdf8 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.Designer.cs @@ -0,0 +1,1474 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20251009202933_AddUserAccountsNavigationProperty")] + partial class AddUserAccountsNavigationProperty + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BacktestCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalFees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName") + .IsUnique(); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Score"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("AccumulatedRunTimeSeconds") + .HasColumnType("bigint"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LastStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStopTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BacktestRequestsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("GasFees") + .HasColumnType("decimal(18,8)"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("NetPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UiFees") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.cs b/src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.cs new file mode 100644 index 00000000..79b2bf37 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251009202933_AddUserAccountsNavigationProperty.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class AddUserAccountsNavigationProperty : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index 24110d57..fff06f99 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -1292,7 +1292,7 @@ namespace Managing.Infrastructure.Databases.Migrations modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => { b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() + .WithMany("Accounts") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.SetNull) .IsRequired(); @@ -1460,6 +1460,11 @@ namespace Managing.Infrastructure.Databases.Migrations { b.Navigation("ScenarioIndicators"); }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs index 99ebd24c..7e8e920d 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs @@ -13,4 +13,7 @@ public class UserEntity [MaxLength(255)] public string? AgentName { get; set; } public string? AvatarUrl { get; set; } public string? TelegramChannel { get; set; } + + // Navigation properties + public virtual ICollection Accounts { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs index dd326abf..1e5deb32 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs @@ -86,7 +86,7 @@ public class ManagingDbContext : DbContext // Configure relationship with User entity.HasOne(e => e.User) - .WithMany() + .WithMany(u => u.Accounts) .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); }); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs index 7c826ee8..b0f62d91 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs @@ -1,6 +1,7 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Accounts; +using Managing.Domain.Users; using Microsoft.EntityFrameworkCore; namespace Managing.Infrastructure.Databases.PostgreSql; @@ -22,6 +23,7 @@ public class PostgreSqlAccountRepository : IAccountRepository { var accountEntity = _context.Accounts .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); if (accountEntity != null) { @@ -37,6 +39,13 @@ public class PostgreSqlAccountRepository : IAccountRepository _cacheService.RemoveValue(keyCacheKey); _cacheService.RemoveValue(idCacheKey); _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 _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 var nameCacheKey = $"account_{account.Name}"; var keyCacheKey = $"account_key_{account.Key}"; @@ -200,6 +213,41 @@ public class PostgreSqlAccountRepository : IAccountRepository _cacheService.SaveValue(idCacheKey, account, TimeSpan.FromHours(1)); } + public async Task> GetAccountsByUserAsync(User user) + { + try + { + // Check cache first for accounts by user + var cacheKey = $"accounts_user_{user.Name}"; + var cachedAccounts = _cacheService.GetValue>(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) { try @@ -229,6 +277,13 @@ public class PostgreSqlAccountRepository : IAccountRepository _cacheService.RemoveValue(keyCacheKey); _cacheService.RemoveValue(idCacheKey); _cacheService.RemoveValue("all_accounts"); + + // Invalidate user-specific accounts cache + if (account.User != null) + { + var userAccountsCacheKey = $"accounts_user_{account.User.Name}"; + _cacheService.RemoveValue(userAccountsCacheKey); + } } finally { diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index da0aec0e..a6227350 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -129,7 +129,27 @@ public static class PostgreSqlMappers AgentName = entity.AgentName, AvatarUrl = entity.AvatarUrl, 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() + }; + } + + // 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(), // Empty list for now, balances handled separately if needed + IsGmxInitialized = entity.IsGmxInitialized }; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs index 2157b26e..41ba0408 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs @@ -1,5 +1,6 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; +using Managing.Domain.Accounts; using Managing.Domain.Users; using Managing.Infrastructure.Databases.PostgreSql.Entities; using Microsoft.EntityFrameworkCore; @@ -66,12 +67,12 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito }, nameof(GetUserByAgentNameAsync), ("agentName", agentName)); } - public async Task GetUserByNameAsync(string name) + public async Task GetUserByNameAsync(string name, bool fetchAccounts = false) { return await ExecuteWithLoggingAsync(async () => { // 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(cacheKey); if (cachedUser != null) { @@ -82,28 +83,61 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); - // Optimized query with explicit SELECT to avoid loading unnecessary data - var userEntity = await _context.Users - .AsNoTracking() - .Where(u => u.Name == name) - .Select(u => new UserEntity + 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) { - Id = u.Id, - Name = u.Name, - AgentName = u.AgentName, - AvatarUrl = u.AvatarUrl, - TelegramChannel = u.TelegramChannel - }) - .FirstOrDefaultAsync() - .ConfigureAwait(false); + user.Accounts = userEntity.Accounts.Select(PostgreSqlMappers.Map).ToList(); + } + else + { + user.Accounts = new List(); + } + } + else + { + // Optimized query with explicit SELECT to avoid loading unnecessary data + var userEntity = await _context.Users + .AsNoTracking() + .Where(u => u.Name == name) + .Select(u => new UserEntity + { + Id = u.Id, + Name = u.Name, + AgentName = u.AgentName, + AvatarUrl = u.AvatarUrl, + TelegramChannel = u.TelegramChannel + }) + .FirstOrDefaultAsync() + .ConfigureAwait(false); - if (userEntity == null) - throw new InvalidOperationException($"User with name '{name}' not found"); + if (userEntity == null) + throw new InvalidOperationException($"User with name '{name}' not found"); - var user = PostgreSqlMappers.Map(userEntity); + user = PostgreSqlMappers.Map(userEntity); + user.Accounts = new List(); // Initialize empty list + } // 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; } @@ -111,7 +145,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito { await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } - }, nameof(GetUserByNameAsync), ("name", name)); + }, nameof(GetUserByNameAsync), ("name", name), ("fetchAccounts", fetchAccounts)); } public async Task> GetAllUsersAsync() @@ -214,7 +248,9 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito // Invalidate cache for updated user - handle both old and new AgentName var nameCacheKey = $"user_name_{user.Name}"; + var nameWithAccountsCacheKey = $"user_name_with_accounts_{user.Name}"; _cacheService.RemoveValue(nameCacheKey); + _cacheService.RemoveValue(nameWithAccountsCacheKey); // Invalidate old AgentName cache if it existed if (!string.IsNullOrEmpty(oldAgentName)) diff --git a/src/Managing.WebApp/src/app/store/userStore.tsx b/src/Managing.WebApp/src/app/store/userStore.tsx index 130b7cc4..226c18fb 100644 --- a/src/Managing.WebApp/src/app/store/userStore.tsx +++ b/src/Managing.WebApp/src/app/store/userStore.tsx @@ -59,7 +59,7 @@ export const useCurrentUser = () => { } store.setLoading(query.isLoading) 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 {