diff --git a/DD.Persistence.API/Controllers/DataSourceSystemController.cs b/DD.Persistence.API/Controllers/DataSourceSystemController.cs index 896af1f..5e551a9 100644 --- a/DD.Persistence.API/Controllers/DataSourceSystemController.cs +++ b/DD.Persistence.API/Controllers/DataSourceSystemController.cs @@ -14,9 +14,9 @@ namespace DD.Persistence.API.Controllers; [Route("api/[controller]")] public class DataSourceSystemController : ControllerBase { - private readonly IDataSourceSystemRepository dataSourceSystemRepository; + private readonly IRelatedDataRepository dataSourceSystemRepository; - public DataSourceSystemController(IDataSourceSystemRepository dataSourceSystemRepository) + public DataSourceSystemController(IRelatedDataRepository dataSourceSystemRepository) { this.dataSourceSystemRepository = dataSourceSystemRepository; } diff --git a/DD.Persistence.API/Controllers/TimestampedValuesController.cs b/DD.Persistence.API/Controllers/TimestampedValuesController.cs index a059243..2b0b245 100644 --- a/DD.Persistence.API/Controllers/TimestampedValuesController.cs +++ b/DD.Persistence.API/Controllers/TimestampedValuesController.cs @@ -15,9 +15,9 @@ namespace DD.Persistence.API.Controllers; [Route("api/[controller]/{discriminatorId}")] public class TimestampedValuesController : ControllerBase { - private readonly ITimestampedValuesRepository repository; + private readonly ITimestampedValuesRepository repository; - public TimestampedValuesController(ITimestampedValuesRepository repository) + public TimestampedValuesController(ITimestampedValuesRepository repository) { this.repository = repository; } diff --git a/DD.Persistence.App/appsettings.Tests.json b/DD.Persistence.App/appsettings.Tests.json index e8d3cc4..9934757 100644 --- a/DD.Persistence.App/appsettings.Tests.json +++ b/DD.Persistence.App/appsettings.Tests.json @@ -4,7 +4,7 @@ "Port": 5432, "Database": "persistence", "Username": "postgres", - "Password": "q" + "Password": "postgres" }, "NeedUseKeyCloak": false, "AuthUser": { diff --git a/DD.Persistence.Database.Postgres/Migrations/20250114100429_Init.Designer.cs b/DD.Persistence.Database.Postgres/Migrations/20250116093615_Init.Designer.cs similarity index 87% rename from DD.Persistence.Database.Postgres/Migrations/20250114100429_Init.Designer.cs rename to DD.Persistence.Database.Postgres/Migrations/20250116093615_Init.Designer.cs index 8d0173d..0fa9552 100644 --- a/DD.Persistence.Database.Postgres/Migrations/20250114100429_Init.Designer.cs +++ b/DD.Persistence.Database.Postgres/Migrations/20250116093615_Init.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace DD.Persistence.Database.Postgres.Migrations { [DbContext(typeof(PersistencePostgresContext))] - [Migration("20250114100429_Init")] + [Migration("20250116093615_Init")] partial class Init { /// @@ -122,7 +122,24 @@ namespace DD.Persistence.Database.Postgres.Migrations b.HasKey("DiscriminatorId", "Timestamp"); - b.ToTable("TimestampedSets"); + b.ToTable("TimestampedValues"); + }); + + modelBuilder.Entity("DD.Persistence.Database.Entity.ValuesIdentity", b => + { + b.Property("DiscriminatorId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Дискриминатор системы"); + + b.Property("Identity") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Идентификаторы"); + + b.HasKey("DiscriminatorId"); + + b.ToTable("ValuesIdentities"); }); modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b => @@ -212,6 +229,17 @@ namespace DD.Persistence.Database.Postgres.Migrations b.Navigation("System"); }); + + modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b => + { + b.HasOne("DD.Persistence.Database.Entity.ValuesIdentity", "ValuesIdentity") + .WithMany() + .HasForeignKey("DiscriminatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ValuesIdentity"); + }); #pragma warning restore 612, 618 } } diff --git a/DD.Persistence.Database.Postgres/Migrations/20250114100429_Init.cs b/DD.Persistence.Database.Postgres/Migrations/20250116093615_Init.cs similarity index 86% rename from DD.Persistence.Database.Postgres/Migrations/20250114100429_Init.cs rename to DD.Persistence.Database.Postgres/Migrations/20250116093615_Init.cs index 999a231..1898472 100644 --- a/DD.Persistence.Database.Postgres/Migrations/20250114100429_Init.cs +++ b/DD.Persistence.Database.Postgres/Migrations/20250116093615_Init.cs @@ -74,16 +74,15 @@ namespace DD.Persistence.Database.Postgres.Migrations }); migrationBuilder.CreateTable( - name: "TimestampedSets", + name: "ValuesIdentities", columns: table => new { - Timestamp = table.Column(type: "timestamp with time zone", nullable: false, comment: "Временная отметка"), DiscriminatorId = table.Column(type: "uuid", nullable: false, comment: "Дискриминатор системы"), - Values = table.Column(type: "jsonb", nullable: false, comment: "Данные") + Identity = table.Column(type: "jsonb", nullable: false, comment: "Идентификаторы") }, constraints: table => { - table.PrimaryKey("PK_TimestampedSets", x => new { x.DiscriminatorId, x.Timestamp }); + table.PrimaryKey("PK_ValuesIdentities", x => x.DiscriminatorId); }); migrationBuilder.CreateTable( @@ -108,6 +107,25 @@ namespace DD.Persistence.Database.Postgres.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "TimestampedValues", + columns: table => new + { + Timestamp = table.Column(type: "timestamp with time zone", nullable: false, comment: "Временная отметка"), + DiscriminatorId = table.Column(type: "uuid", nullable: false, comment: "Дискриминатор системы"), + Values = table.Column(type: "jsonb", nullable: false, comment: "Данные") + }, + constraints: table => + { + table.PrimaryKey("PK_TimestampedValues", x => new { x.DiscriminatorId, x.Timestamp }); + table.ForeignKey( + name: "FK_TimestampedValues_ValuesIdentities_DiscriminatorId", + column: x => x.DiscriminatorId, + principalTable: "ValuesIdentities", + principalColumn: "DiscriminatorId", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX_TechMessage_SystemId", table: "TechMessage", @@ -130,10 +148,13 @@ namespace DD.Persistence.Database.Postgres.Migrations name: "TechMessage"); migrationBuilder.DropTable( - name: "TimestampedSets"); + name: "TimestampedValues"); migrationBuilder.DropTable( name: "DataSourceSystem"); + + migrationBuilder.DropTable( + name: "ValuesIdentities"); } } } diff --git a/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs b/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs index 6e10f12..72e5d62 100644 --- a/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs +++ b/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs @@ -119,7 +119,24 @@ namespace DD.Persistence.Database.Postgres.Migrations b.HasKey("DiscriminatorId", "Timestamp"); - b.ToTable("TimestampedSets"); + b.ToTable("TimestampedValues"); + }); + + modelBuilder.Entity("DD.Persistence.Database.Entity.ValuesIdentity", b => + { + b.Property("DiscriminatorId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Дискриминатор системы"); + + b.Property("Identity") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Идентификаторы"); + + b.HasKey("DiscriminatorId"); + + b.ToTable("ValuesIdentities"); }); modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b => @@ -209,6 +226,17 @@ namespace DD.Persistence.Database.Postgres.Migrations b.Navigation("System"); }); + + modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b => + { + b.HasOne("DD.Persistence.Database.Entity.ValuesIdentity", "ValuesIdentity") + .WithMany() + .HasForeignKey("DiscriminatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ValuesIdentity"); + }); #pragma warning restore 612, 618 } } diff --git a/DD.Persistence.Database/Entity/DataSourceSystem.cs b/DD.Persistence.Database/Entity/DataSourceSystem.cs index d200731..cc282ac 100644 --- a/DD.Persistence.Database/Entity/DataSourceSystem.cs +++ b/DD.Persistence.Database/Entity/DataSourceSystem.cs @@ -9,7 +9,7 @@ public class DataSourceSystem public Guid SystemId { get; set; } [Required, Column(TypeName = "varchar(256)"), Comment("Наименование системы - источника данных")] - public required string Name { get; set; } + public string Name { get; set; } = string.Empty; [Comment("Описание системы - источника данных")] public string? Description { get; set; } diff --git a/DD.Persistence.Database/Entity/TimestampedValues.cs b/DD.Persistence.Database/Entity/TimestampedValues.cs index 3205203..05c7a57 100644 --- a/DD.Persistence.Database/Entity/TimestampedValues.cs +++ b/DD.Persistence.Database/Entity/TimestampedValues.cs @@ -16,4 +16,7 @@ public class TimestampedValues : ITimestampedItem [Comment("Данные"), Column(TypeName = "jsonb")] public required object[] Values { get; set; } + + [Required, ForeignKey(nameof(DiscriminatorId)), Comment("Идентификаторы")] + public virtual ValuesIdentity? ValuesIdentity { get; set; } } diff --git a/DD.Persistence.Database/Entity/ValuesIdentity.cs b/DD.Persistence.Database/Entity/ValuesIdentity.cs new file mode 100644 index 0000000..dc7f607 --- /dev/null +++ b/DD.Persistence.Database/Entity/ValuesIdentity.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace DD.Persistence.Database.Entity; + +public class ValuesIdentity +{ + [Key, Comment("Дискриминатор системы"),] + public Guid DiscriminatorId { get; set; } + + [Comment("Идентификаторы"), Column(TypeName = "jsonb")] + public string[] Identity { get; set; } = []; +} diff --git a/DD.Persistence.Database/PersistenceDbContext.cs b/DD.Persistence.Database/PersistenceDbContext.cs index 0cddcba..18a01cc 100644 --- a/DD.Persistence.Database/PersistenceDbContext.cs +++ b/DD.Persistence.Database/PersistenceDbContext.cs @@ -11,7 +11,9 @@ public class PersistenceDbContext : DbContext { public DbSet Setpoint => Set(); - public DbSet TimestampedSets => Set(); + public DbSet ValuesIdentities => Set(); + + public DbSet TimestampedValues => Set(); public DbSet ChangeLog => Set(); @@ -29,6 +31,10 @@ public class PersistenceDbContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity() + .Property(e => e.Identity) + .HasJsonConversion(); + modelBuilder.Entity() .Property(e => e.Values) .HasJsonConversion(); diff --git a/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs index a8135c6..97882e5 100644 --- a/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs @@ -212,11 +212,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest }; string jsonString = JsonSerializer.Serialize(t); var values = JsonSerializer.Deserialize(jsonString); - //// - //dynamic obj = new ExpandoObject(); - //// "FieldName" 123 - //obj.FieldName = 123; yield return new TimestampedValuesDto() { diff --git a/DD.Persistence.Repository/DependencyInjection.cs b/DD.Persistence.Repository/DependencyInjection.cs index 492c502..498849a 100644 --- a/DD.Persistence.Repository/DependencyInjection.cs +++ b/DD.Persistence.Repository/DependencyInjection.cs @@ -38,11 +38,14 @@ public static class DependencyInjection services.AddTransient(); services.AddTransient(); - services.AddTransient, TimestampedValuesRepository>(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient, + RelatedDataCachedRepository>(); + services.AddTransient, + RelatedDataCachedRepository>(); - return services; + return services; } } diff --git a/DD.Persistence.Repository/Repositories/DataSourceSystemRepository.cs b/DD.Persistence.Repository/Repositories/DataSourceSystemRepository.cs deleted file mode 100644 index d8b6c0a..0000000 --- a/DD.Persistence.Repository/Repositories/DataSourceSystemRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using DD.Persistence.Database.Entity; -using DD.Persistence.Models; -using DD.Persistence.Repositories; -using Mapster; -using Microsoft.EntityFrameworkCore; - -namespace DD.Persistence.Repository.Repositories; -public class DataSourceSystemRepository : IDataSourceSystemRepository -{ - protected DbContext db; - public DataSourceSystemRepository(DbContext db) - { - this.db = db; - } - protected virtual IQueryable GetQueryReadOnly() => db.Set(); - - public virtual async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token) - { - var entity = dataSourceSystemDto.Adapt(); - - await db.Set().AddAsync(entity, token); - await db.SaveChangesAsync(token); - } - - public virtual async Task> Get(CancellationToken token) - { - var query = GetQueryReadOnly(); - var entities = await query.ToArrayAsync(token); - var dtos = entities.Select(e => e.Adapt()); - - return dtos; - } -} diff --git a/DD.Persistence.Repository/Repositories/RelatedDataRepository.cs b/DD.Persistence.Repository/Repositories/RelatedDataRepository.cs new file mode 100644 index 0000000..7efc329 --- /dev/null +++ b/DD.Persistence.Repository/Repositories/RelatedDataRepository.cs @@ -0,0 +1,33 @@ +using DD.Persistence.Repositories; +using Mapster; +using Microsoft.EntityFrameworkCore; + +namespace DD.Persistence.Repository.Repositories; +public class RelatedDataRepository : IRelatedDataRepository + where TDto : class, new() + where TEntity : class, new() +{ + protected DbContext db; + public RelatedDataRepository(DbContext db) + { + this.db = db; + } + protected virtual IQueryable GetQueryReadOnly() => db.Set(); + + public virtual async Task Add(TDto dataSourceSystemDto, CancellationToken token) + { + var entity = dataSourceSystemDto.Adapt(); + + await db.Set().AddAsync(entity, token); + await db.SaveChangesAsync(token); + } + + public virtual async Task> Get(CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query.ToArrayAsync(token); + var dtos = entities.Select(e => e.Adapt()); + + return dtos; + } +} diff --git a/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs b/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs index 2917021..274a940 100644 --- a/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs +++ b/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs @@ -1,22 +1,19 @@ -using Mapster; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json.Linq; using DD.Persistence.Database.Entity; using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; using DD.Persistence.Repositories; -using UuidExtensions; -using DD.Persistence.Models.Common; +using Mapster; +using Microsoft.EntityFrameworkCore; namespace DD.Persistence.Repository.Repositories { public class TechMessagesRepository : ITechMessagesRepository { - private readonly IDataSourceSystemRepository sourceSystemRepository; + private readonly IRelatedDataRepository sourceSystemRepository; private DbContext db; - public TechMessagesRepository(DbContext db, IDataSourceSystemRepository sourceSystemRepository) + public TechMessagesRepository(DbContext db, IRelatedDataRepository sourceSystemRepository) { this.db = db; this.sourceSystemRepository = sourceSystemRepository; diff --git a/DD.Persistence.Repository/Repositories/TimestampedSetRepository.cs b/DD.Persistence.Repository/Repositories/TimestampedSetRepository.cs deleted file mode 100644 index 73225be..0000000 --- a/DD.Persistence.Repository/Repositories/TimestampedSetRepository.cs +++ /dev/null @@ -1,122 +0,0 @@ -//using Microsoft.EntityFrameworkCore; -//using DD.Persistence.Database.Entity; -//using DD.Persistence.Models; -//using DD.Persistence.Repositories; -//using DD.Persistence.Models.Common; - -//namespace DD.Persistence.Repository.Repositories; - -///// -///// Репозиторий для хранения разных наборов данных временных рядов. -///// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. -///// idDiscriminator формируют клиенты и только им известно что они обозначают. -///// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. -///// -//public class TimestampedSetRepository : ITimestampedSetRepository -//{ -// private readonly DbContext db; - -// public TimestampedSetRepository(DbContext db) -// { -// this.db = db; -// } - -// public Task AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token) -// { -// var entities = sets.Select(set => new TimestampedSet(idDiscriminator, set.Timestamp.ToUniversalTime(), set.Set)); -// var dbSet = db.Set(); -// dbSet.AddRange(entities); -// return db.SaveChangesAsync(token); -// } - -// public async Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) -// { -// var dbSet = db.Set(); -// var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); - -// if (geTimestamp.HasValue) -// query = ApplyGeTimestamp(query, geTimestamp.Value); - -// query = query -// .OrderBy(item => item.Timestamp) -// .Skip(skip) -// .Take(take); - -// var data = await Materialize(query, token); - -// if (columnNames is not null && columnNames.Any()) -// data = ReduceSetColumnsByNames(data, columnNames); - -// return data; -// } - -// public async Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token) -// { -// var dbSet = db.Set(); -// var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); - -// query = query.OrderByDescending(entity => entity.Timestamp) -// .Take(take) -// .OrderBy(entity => entity.Timestamp); - -// var data = await Materialize(query, token); - -// if (columnNames is not null && columnNames.Any()) -// data = ReduceSetColumnsByNames(data, columnNames); - -// return data; -// } - -// public Task Count(Guid idDiscriminator, CancellationToken token) -// { -// var dbSet = db.Set(); -// var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); -// return query.CountAsync(token); -// } - -// public async Task GetDatesRange(Guid idDiscriminator, CancellationToken token) -// { -// var query = db.Set() -// .GroupBy(entity => entity.IdDiscriminator) -// .Select(group => new -// { -// Min = group.Min(entity => entity.Timestamp), -// Max = group.Max(entity => entity.Timestamp), -// }); - -// var item = await query.FirstOrDefaultAsync(token); -// if (item is null) -// return null; - -// return new DatesRangeDto -// { -// From = item.Min, -// To = item.Max, -// }; -// } - -// private static async Task> Materialize(IQueryable query, CancellationToken token) -// { -// var dtoQuery = query.Select(entity => new TimestampedSetDto(entity.Timestamp, entity.Set)); -// var dtos = await dtoQuery.ToArrayAsync(token); -// return dtos; -// } - -// private static IQueryable ApplyGeTimestamp(IQueryable query, DateTimeOffset geTimestamp) -// { -// var geTimestampUtc = geTimestamp.ToUniversalTime(); -// return query.Where(entity => entity.Timestamp >= geTimestampUtc); -// } - -// private static IEnumerable ReduceSetColumnsByNames(IEnumerable query, IEnumerable columnNames) -// { -// var newQuery = query -// .Select(entity => new TimestampedSetDto( -// entity.Timestamp, -// entity.Set -// .Where(prop => columnNames.Contains(prop.Key)) -// .ToDictionary(prop => prop.Key, prop => prop.Value) -// )); -// return newQuery; -// } -//} diff --git a/DD.Persistence.Repository/Repositories/TimestampedValuesRepository.cs b/DD.Persistence.Repository/Repositories/TimestampedValuesRepository.cs index cc892ad..9be62db 100644 --- a/DD.Persistence.Repository/Repositories/TimestampedValuesRepository.cs +++ b/DD.Persistence.Repository/Repositories/TimestampedValuesRepository.cs @@ -1,24 +1,29 @@ using DD.Persistence.Database.Entity; using DD.Persistence.Models; using DD.Persistence.Models.Common; -using DD.Persistence.ModelsAbstractions; using DD.Persistence.Repositories; +using DD.Persistence.Repository.Extensions; using Mapster; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; +using System.Linq; using System.Text.Json; +using System.Text.Json.Nodes; namespace DD.Persistence.Repository.Repositories; -public class TimestampedValuesRepository : ITimestampedValuesRepository - where TDto : class, ITimestampAbstractDto, new() +public class TimestampedValuesRepository : ITimestampedValuesRepository { private readonly DbContext db; + private readonly IRelatedDataRepository relatedDataRepository; - public TimestampedValuesRepository(DbContext db) + public TimestampedValuesRepository(DbContext db, IRelatedDataRepository relatedDataRepository) { this.db = db; + this.relatedDataRepository = relatedDataRepository; } - protected virtual IQueryable GetQueryReadOnly() => this.db.Set(); + protected virtual IQueryable GetQueryReadOnly() => this.db.Set() + .Include(e => e.ValuesIdentity); public virtual async Task GetDatesRange(Guid discriminatorId, CancellationToken token) { @@ -41,47 +46,57 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository> GetGtDate(Guid discriminatorId, DateTimeOffset date, CancellationToken token) + public virtual async Task> GetGtDate(Guid discriminatorId, DateTimeOffset date, CancellationToken token) { var query = GetQueryReadOnly().Where(e => e.Timestamp > date); var entities = await query.ToArrayAsync(token); - var dtos = entities.Select(e => e.Adapt()); + var dtos = entities.Select(e => e.Adapt()); return dtos; } - public virtual async Task AddRange(Guid discriminatorId, IEnumerable dtos, CancellationToken token) + public virtual async Task AddRange(Guid discriminatorId, IEnumerable dtos, CancellationToken token) { - var entities = dtos - .Select(d => d.Adapt()) - .ToList(); + var timestampedValuesEntities = new List(); + foreach (var dto in dtos) + { + var values = dto.Values + .SelectMany(v => JsonSerializer.Deserialize>(v.ToString()!)!) + .ToDictionary(); - entities.ForEach(d => - { - d.DiscriminatorId = discriminatorId; - d.Timestamp = d.Timestamp.ToUniversalTime(); - }); + var keys = values.Keys.ToArray(); + await CreateValuesIdentityIfNotExist(discriminatorId, keys, token); + + var timestampedValuesEntity = new TimestampedValues() + { + DiscriminatorId = discriminatorId, + Timestamp = dto.Timestamp.ToUniversalTime(), + Values = values.Values.ToArray() + }; + timestampedValuesEntities.Add(timestampedValuesEntity); + } + + await db.Set().AddRangeAsync(timestampedValuesEntities, token); - await db.Set().AddRangeAsync(entities, token); var result = await db.SaveChangesAsync(token); return result; } - protected async Task> GetLastAsync(int takeCount, CancellationToken token) + protected async Task> GetLastAsync(int takeCount, CancellationToken token) { var query = GetQueryReadOnly() .OrderByDescending(e => e.Timestamp) .Take(takeCount); var entities = await query.ToArrayAsync(token); - var dtos = entities.Select(e => e.Adapt()); + var dtos = entities.Select(e => e.Adapt()); return dtos; } - protected async Task GetFirstAsync(CancellationToken token) + protected async Task GetFirstAsync(CancellationToken token) { var query = GetQueryReadOnly() .OrderBy(e => e.Timestamp); @@ -91,11 +106,11 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository(); + var dto = entity.Adapt(); return dto; } - public async virtual Task> GetResampledData( + public async virtual Task> GetResampledData( Guid discriminatorId, DateTimeOffset dateBegin, double intervalSec = 600d, @@ -116,10 +131,10 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) + public async Task> Get(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) { var dbSet = db.Set(); - var query = dbSet.Where(entity => entity.DiscriminatorId == idDiscriminator); + var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId); if (geTimestamp.HasValue) query = ApplyGeTimestamp(query, geTimestamp.Value); @@ -129,7 +144,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository : ITimestampedValuesRepository> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token) + public async Task> GetLast(Guid discriminatorId, IEnumerable? columnNames, int take, CancellationToken token) { var dbSet = db.Set(); - var query = dbSet.Where(entity => entity.DiscriminatorId == idDiscriminator); + var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId); query = query.OrderByDescending(entity => entity.Timestamp) .Take(take) .OrderBy(entity => entity.Timestamp); - var data = await Materialize(query, token); + var data = await Materialize(discriminatorId, query, token); if (columnNames is not null && columnNames.Any()) data = ReduceSetColumnsByNames(data, columnNames); @@ -154,21 +169,43 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository Count(Guid idDiscriminator, CancellationToken token) + public Task Count(Guid discriminatorId, CancellationToken token) { var dbSet = db.Set(); - var query = dbSet.Where(entity => entity.DiscriminatorId == idDiscriminator); + var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId); return query.CountAsync(token); } - private static async Task> Materialize(IQueryable query, CancellationToken token) + private async Task> Materialize(Guid discriminatorId, IQueryable query, CancellationToken token) { - var dtoQuery = query.Select(entity => new TimestampedValuesDto() { Timestamp = entity.Timestamp, Values = entity.Values }); + var dtoQuery = query.Select(entity => new TimestampedValuesDto() + { + Timestamp = entity.Timestamp, + Values = entity.Values + }); + var dtos = await dtoQuery.ToArrayAsync(token); + foreach(var dto in dtos) + { + var valuesIdentities = await relatedDataRepository.Get(token); + var valuesIdentity = valuesIdentities? + .FirstOrDefault(e => e.DiscriminatorId == discriminatorId); + if (valuesIdentity == null) + return []; // ToDo: какая логика должна быть? + + for (var i = 0; i < valuesIdentity.Identity.Count(); i++) + { + var key = valuesIdentity.Identity[i]; + var value = dto.Values[i]; + + dto.Values[i] = new { key = value }; // ToDo: вывод? + } + } + return dtos; } - private static IQueryable ApplyGeTimestamp(IQueryable query, DateTimeOffset geTimestamp) + private IQueryable ApplyGeTimestamp(IQueryable query, DateTimeOffset geTimestamp) { var geTimestampUtc = geTimestamp.ToUniversalTime(); return query.Where(entity => entity.Timestamp >= geTimestampUtc); @@ -176,16 +213,43 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository ReduceSetColumnsByNames(IEnumerable query, IEnumerable columnNames) { - var newQuery = query - .Select(entity => new TimestampedValuesDto() - { - Timestamp = entity.Timestamp, - Values = entity.Values? - .Where(prop => columnNames.Contains( - JsonSerializer.Deserialize>(prop.ToString()!)? - .FirstOrDefault().Key - )).ToArray() - }); + var newQuery = query; + //.Select(entity => new TimestampedValuesDto() + //{ + // Timestamp = entity.Timestamp, + // Values = entity.Values? + // .Where(prop => columnNames.Contains( + // JsonSerializer.Deserialize>(prop.ToString()!)? + // .FirstOrDefault().Key + // )).ToArray() + //}); return newQuery; } + + private async Task CreateValuesIdentityIfNotExist(Guid discriminatorId, string[] keys, CancellationToken token) + { + var valuesIdentities = await relatedDataRepository.Get(token); + var valuesIdentity = valuesIdentities? + .FirstOrDefault(e => e.DiscriminatorId == discriminatorId); + + if (valuesIdentity == null) + { + valuesIdentity = new ValuesIdentityDto() + { + DiscriminatorId = discriminatorId, + Identity = keys + }; + await relatedDataRepository.Add(valuesIdentity, token); + + return; + } + + if (!valuesIdentity.Identity.SequenceEqual(keys)) + { + var expectedIdentity = string.Join(", ", valuesIdentity.Identity); + var actualIdentity = string.Join(", ", keys); + throw new InvalidOperationException($"Для системы {discriminatorId.ToString()} " + + $"характерен набор данных: [{expectedIdentity}], однако был передан набор: [{actualIdentity}]"); + } + } } diff --git a/DD.Persistence.Repository/RepositoriesCached/DataSourceSystemCachedRepository.cs b/DD.Persistence.Repository/RepositoriesCached/RelatedDataCachedRepository.cs similarity index 70% rename from DD.Persistence.Repository/RepositoriesCached/DataSourceSystemCachedRepository.cs rename to DD.Persistence.Repository/RepositoriesCached/RelatedDataCachedRepository.cs index febb65e..bc680f0 100644 --- a/DD.Persistence.Repository/RepositoriesCached/DataSourceSystemCachedRepository.cs +++ b/DD.Persistence.Repository/RepositoriesCached/RelatedDataCachedRepository.cs @@ -4,24 +4,26 @@ using DD.Persistence.Models; using DD.Persistence.Repository.Repositories; namespace DD.Persistence.Repository.RepositoriesCached; -public class DataSourceSystemCachedRepository : DataSourceSystemRepository +public class RelatedDataCachedRepository : RelatedDataRepository + where TEntity : class, new() + where TDto : class, new() { private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DataSourceSystem).FullName}CacheKey"; private readonly IMemoryCache memoryCache; private const int CacheExpirationInMinutes = 60; private readonly TimeSpan? AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60); - public DataSourceSystemCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db) + public RelatedDataCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db) { this.memoryCache = memoryCache; } - public override async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token) + public override async Task Add(TDto dataSourceSystemDto, CancellationToken token) { await base.Add(dataSourceSystemDto, token); memoryCache.Remove(SystemCacheKey); } - public override async Task> Get(CancellationToken token) + public override async Task> Get(CancellationToken token) { var systems = await memoryCache.GetOrCreateAsync(SystemCacheKey, async (cacheEntry) => { diff --git a/DD.Persistence/Models/DataSourceSystemDto.cs b/DD.Persistence/Models/DataSourceSystemDto.cs index 4abe706..2726e91 100644 --- a/DD.Persistence/Models/DataSourceSystemDto.cs +++ b/DD.Persistence/Models/DataSourceSystemDto.cs @@ -13,7 +13,7 @@ public class DataSourceSystemDto /// /// Наименование /// - public required string Name { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; /// /// Описание diff --git a/DD.Persistence/Models/TimestampedValuesDto.cs b/DD.Persistence/Models/TimestampedValuesDto.cs index 938970d..d30b549 100644 --- a/DD.Persistence/Models/TimestampedValuesDto.cs +++ b/DD.Persistence/Models/TimestampedValuesDto.cs @@ -15,5 +15,5 @@ public class TimestampedValuesDto : ITimestampAbstractDto /// /// Набор данных /// - public object[]? Values { get; set; } + public object[] Values { get; set; } = []; } diff --git a/DD.Persistence/Models/ValuesIdentityDto.cs b/DD.Persistence/Models/ValuesIdentityDto.cs new file mode 100644 index 0000000..36d91fc --- /dev/null +++ b/DD.Persistence/Models/ValuesIdentityDto.cs @@ -0,0 +1,17 @@ +namespace DD.Persistence.Models; + +/// +/// Набор идентификаторов для набора данных +/// +public class ValuesIdentityDto +{ + /// + /// Дискриминатор системы + /// + public Guid DiscriminatorId { get; set; } + + /// + /// Идентификаторы + /// + public string[] Identity { get; set; } = []; +} diff --git a/DD.Persistence/Repositories/IDataSourceSystemRepository.cs b/DD.Persistence/Repositories/IDataSourceSystemRepository.cs deleted file mode 100644 index ce674d6..0000000 --- a/DD.Persistence/Repositories/IDataSourceSystemRepository.cs +++ /dev/null @@ -1,22 +0,0 @@ -using DD.Persistence.Models; - -namespace DD.Persistence.Repositories; - -/// -/// Интерфейс по работе с системами - источниками данных -/// -public interface IDataSourceSystemRepository -{ - /// - /// Добавить систему - /// - /// - /// - public Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token); - - /// - /// Получить список систем - /// - /// - public Task> Get(CancellationToken token); -} diff --git a/DD.Persistence/Repositories/IRelatedDataRepository.cs b/DD.Persistence/Repositories/IRelatedDataRepository.cs new file mode 100644 index 0000000..a5f5fbc --- /dev/null +++ b/DD.Persistence/Repositories/IRelatedDataRepository.cs @@ -0,0 +1,23 @@ +using DD.Persistence.Models; + +namespace DD.Persistence.Repositories; + +/// +/// Интерфейс по работе с системами - источниками данных +/// +public interface IRelatedDataRepository +{ + /// + /// Добавить систему + /// + /// + /// + /// + public Task Add(TDto dataSourceSystemDto, CancellationToken token); + + /// + /// Получить список систем + /// + /// + public Task> Get(CancellationToken token); +} diff --git a/DD.Persistence/Repositories/ITimestampedValuesRepository.cs b/DD.Persistence/Repositories/ITimestampedValuesRepository.cs index 5a508c4..72cef69 100644 --- a/DD.Persistence/Repositories/ITimestampedValuesRepository.cs +++ b/DD.Persistence/Repositories/ITimestampedValuesRepository.cs @@ -7,9 +7,7 @@ namespace DD.Persistence.Repositories; /// /// Интерфейс по работе с временными данными /// -/// -public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBaseRepository - where TDto : class, ITimestampAbstractDto, new() +public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBaseRepository { /// /// Добавление записей @@ -18,7 +16,7 @@ public interface ITimestampedValuesRepository : ISyncRepository, ITi /// /// /// - Task AddRange(Guid idDiscriminator, IEnumerable dtos, CancellationToken token); + Task AddRange(Guid idDiscriminator, IEnumerable dtos, CancellationToken token); /// /// Количество записей по указанному набору в БД. Для пагинации. diff --git a/DD.Persistence/RepositoriesAbstractions/ISyncRepository.cs b/DD.Persistence/RepositoriesAbstractions/ISyncRepository.cs index 4db2645..d60e0ce 100644 --- a/DD.Persistence/RepositoriesAbstractions/ISyncRepository.cs +++ b/DD.Persistence/RepositoriesAbstractions/ISyncRepository.cs @@ -1,12 +1,12 @@ -using DD.Persistence.Models.Common; +using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.RepositoriesAbstractions; /// /// Интерфейс по работе с данными /// -/// -public interface ISyncRepository +public interface ISyncRepository { /// /// Получить данные, начиная с определенной даты @@ -15,7 +15,7 @@ public interface ISyncRepository /// дата начала /// /// - Task> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token); + Task> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token); /// diff --git a/DD.Persistence/RepositoriesAbstractions/ITimeSeriesBaseRepository.cs b/DD.Persistence/RepositoriesAbstractions/ITimeSeriesBaseRepository.cs index 9fd43b4..b8c6791 100644 --- a/DD.Persistence/RepositoriesAbstractions/ITimeSeriesBaseRepository.cs +++ b/DD.Persistence/RepositoriesAbstractions/ITimeSeriesBaseRepository.cs @@ -5,7 +5,7 @@ namespace DD.Persistence.RepositoriesAbstractions; /// /// Интерфейс по работе с прореженными данными /// -public interface ITimeSeriesBaseRepository +public interface ITimeSeriesBaseRepository { /// /// Получить список объектов с прореживанием @@ -16,7 +16,7 @@ public interface ITimeSeriesBaseRepository /// /// /// - Task> GetResampledData( + Task> GetResampledData( Guid discriminatorId, DateTimeOffset dateBegin, double intervalSec = 600d,