Merge branch 'RepositoriesRework' into FilterBuilder

This commit is contained in:
Roman Efremov 2025-02-06 09:25:08 +05:00
commit 5abfcc0d50
33 changed files with 298 additions and 300 deletions

View File

@ -1,4 +1,5 @@
using DD.Persistence.Database.Model; using DD.Persistence.Database;
using DD.Persistence.Database.Model;
using DD.Persistence.Database.Postgres.Extensions; using DD.Persistence.Database.Postgres.Extensions;
namespace DD.Persistence.API; namespace DD.Persistence.API;
@ -13,7 +14,7 @@ public class Startup
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// Add services to the container. // AddRange services to the container.
services.AddControllers(); services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

View File

@ -7,13 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -24,37 +24,4 @@ public static class DependencyInjection
return services; return services;
} }
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
TypeAdapterConfig<ChangeLog, ChangeLogDto>.NewConfig()
.Map(dest => dest.Value, src => new ChangeLogValuesDto()
{
Value = src.Value,
Id = src.Id
});
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
typeAdapterConfig.RuleMap.Clear();
typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
MapsterSetup();
services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<IDataSchemeRepository, DataSchemeCachedRepository>();
return services;
}
} }

View File

@ -13,7 +13,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DD.Persistence.Database.Postgres.Migrations namespace DD.Persistence.Database.Postgres.Migrations
{ {
[DbContext(typeof(PersistencePostgresContext))] [DbContext(typeof(PersistencePostgresContext))]
[Migration("20250204044050_Init")] [Migration("20250205114037_Init")]
partial class Init partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -67,28 +67,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("change_log"); b.ToTable("change_log");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{
b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<string>("PropNames")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Наименования полей в порядке индексации");
b.PrimitiveCollection<int[]>("PropTypes")
.IsRequired()
.HasColumnType("integer[]")
.HasComment("Типы полей в порядке индексации");
b.HasKey("DiscriminatorId");
b.ToTable("data_scheme");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{ {
b.Property<Guid>("SystemId") b.Property<Guid>("SystemId")
@ -134,6 +112,30 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("parameter_data"); b.ToTable("parameter_data");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b => modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
{ {
b.Property<Guid>("Key") b.Property<Guid>("Key")
@ -222,17 +224,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System"); b.Navigation("System");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DataScheme");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -30,19 +30,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_change_log", x => x.Id); table.PrimaryKey("PK_change_log", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "data_scheme",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
PropNames = table.Column<string>(type: "jsonb", nullable: false, comment: "Наименования полей в порядке индексации"),
PropTypes = table.Column<int[]>(type: "integer[]", nullable: false, comment: "Типы полей в порядке индексации")
},
constraints: table =>
{
table.PrimaryKey("PK_data_scheme", x => x.DiscriminatorId);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "data_source_system", name: "data_source_system",
columns: table => new columns: table => new
@ -70,6 +57,20 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_parameter_data", x => new { x.DiscriminatorId, x.ParameterId, x.Timestamp }); table.PrimaryKey("PK_parameter_data", x => new { x.DiscriminatorId, x.ParameterId, x.Timestamp });
}); });
migrationBuilder.CreateTable(
name: "scheme_property",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
Index = table.Column<int>(type: "integer", nullable: false, comment: "Индекс поля"),
PropertyName = table.Column<string>(type: "text", nullable: false, comment: "Наименования индексируемого поля"),
PropertyKind = table.Column<byte>(type: "smallint", nullable: false, comment: "Тип индексируемого поля")
},
constraints: table =>
{
table.PrimaryKey("PK_scheme_property", x => new { x.DiscriminatorId, x.Index });
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "setpoint", name: "setpoint",
columns: table => new columns: table => new
@ -95,12 +96,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp }); table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp });
table.ForeignKey(
name: "FK_timestamped_values_data_scheme_DiscriminatorId",
column: x => x.DiscriminatorId,
principalTable: "data_scheme",
principalColumn: "DiscriminatorId",
onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@ -140,6 +135,9 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "parameter_data"); name: "parameter_data");
migrationBuilder.DropTable(
name: "scheme_property");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "setpoint"); name: "setpoint");
@ -151,9 +149,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "data_source_system"); name: "data_source_system");
migrationBuilder.DropTable(
name: "data_scheme");
} }
} }
} }

View File

@ -64,28 +64,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("change_log"); b.ToTable("change_log");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{
b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<string>("PropNames")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Наименования полей в порядке индексации");
b.PrimitiveCollection<int[]>("PropTypes")
.IsRequired()
.HasColumnType("integer[]")
.HasComment("Типы полей в порядке индексации");
b.HasKey("DiscriminatorId");
b.ToTable("data_scheme");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{ {
b.Property<Guid>("SystemId") b.Property<Guid>("SystemId")
@ -131,6 +109,30 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("parameter_data"); b.ToTable("parameter_data");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b => modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
{ {
b.Property<Guid>("Key") b.Property<Guid>("Key")
@ -219,17 +221,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System"); b.Navigation("System");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DataScheme");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -1,34 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Postgres.Repositories;
public class DataSchemeRepository : IDataSchemeRepository
{
protected DbContext db;
public DataSchemeRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<DataScheme> GetQueryReadOnly() => db.Set<DataScheme>();
public virtual async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
var entity = dataSourceSystemDto.Adapt<DataScheme>();
await db.Set<DataScheme>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entity = await query.ToArrayAsync();
var dto = entity.Select(e => e.Adapt<DataSchemeDto>()).FirstOrDefault();
return dto;
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
@ -7,12 +7,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,54 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Database.Postgres.RepositoriesCached;
using DD.Persistence.Database.Repositories;
using DD.Persistence.Database.RepositoriesCached;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DD.Persistence.Database;
public static class DependencyInjection
{
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
TypeAdapterConfig<ChangeLog, ChangeLogDto>.NewConfig()
.Map(dest => dest.Value, src => new ChangeLogValuesDto()
{
Value = src.Value,
Id = src.Id
});
TypeAdapterConfig<KeyValuePair<Guid, SchemePropertyDto>, SchemeProperty>.NewConfig()
.Map(dest => dest.DiscriminatorId, src => src.Key)
.Map(dest => dest.Index, src => src.Value.Index)
.Map(dest => dest.PropertyKind, src => src.Value.PropertyKind)
.Map(dest => dest.PropertyName, src => src.Value.PropertyName);
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
typeAdapterConfig.RuleMap.Clear();
typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
MapsterSetup();
services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<ISchemePropertyRepository, SchemePropertyCachedRepository>();
return services;
}
}

View File

@ -1,19 +0,0 @@
using DD.Persistence.Models;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DD.Persistence.Database.Entity;
[Table("data_scheme")]
public class DataScheme
{
[Key, Comment("Идентификатор схемы данных"),]
public Guid DiscriminatorId { get; set; }
[Comment("Наименования полей в порядке индексации"), Column(TypeName = "jsonb")]
public string[] PropNames { get; set; } = [];
[Comment("Типы полей в порядке индексации")]
public PropTypeEnum[] PropTypes { get; set; } = [];
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace DD.Persistence.Database.Entity;
[Table("scheme_property")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Index))]
public class SchemeProperty
{
[Comment("Идентификатор схемы данных")]
public Guid DiscriminatorId { get; set; }
[Comment("Индекс поля")]
public int Index { get; set; }
[Comment("Наименования индексируемого поля")]
public required string PropertyName { get; set; }
[Comment("Тип индексируемого поля")]
public required JsonValueKind PropertyKind { get; set; }
}

View File

@ -17,7 +17,4 @@ public class TimestampedValues : ITimestampedItem, IValuesItem
[Comment("Данные"), Column(TypeName = "jsonb")] [Comment("Данные"), Column(TypeName = "jsonb")]
public required object[] Values { get; set; } public required object[] Values { get; set; }
[Required, ForeignKey(nameof(DiscriminatorId)), Comment("Идентификаторы")]
public virtual DataScheme? DataScheme { get; set; }
} }

View File

@ -2,7 +2,7 @@ using System.Collections.Concurrent;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
namespace DD.Persistence.Database.Postgres.Extensions; namespace DD.Persistence.Database.Extensions;
public static class EFExtensionsSortBy public static class EFExtensionsSortBy
{ {

View File

@ -10,7 +10,7 @@ public class PersistenceDbContext : DbContext
{ {
public DbSet<Setpoint> Setpoint => Set<Setpoint>(); public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<DataScheme> DataSchemes => Set<DataScheme>(); public DbSet<SchemeProperty> SchemeProperty => Set<SchemeProperty>();
public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>(); public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>();
@ -30,10 +30,6 @@ public class PersistenceDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<DataScheme>()
.Property(e => e.PropNames)
.HasJsonConversion();
modelBuilder.Entity<TimestampedValues>() modelBuilder.Entity<TimestampedValues>()
.Property(e => e.Values) .Property(e => e.Values)
.HasJsonConversion(); .HasJsonConversion();

View File

@ -8,7 +8,7 @@ using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using UuidExtensions; using UuidExtensions;
namespace DD.Persistence.Database.Postgres.Repositories; namespace DD.Persistence.Database.Repositories;
public class ChangeLogRepository : IChangeLogRepository public class ChangeLogRepository : IChangeLogRepository
{ {
private readonly DbContext db; private readonly DbContext db;
@ -186,7 +186,7 @@ public class ChangeLogRepository : IChangeLogRepository
var datesUpdate = await datesUpdateQuery.ToArrayAsync(token); var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);
var dates = Enumerable.Concat(datesCreate, datesUpdate); var dates = datesCreate.Concat(datesUpdate);
var datesOnly = dates var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day)) .Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct() .Distinct()
@ -214,7 +214,7 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token) public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{ {
var date = dateBegin.ToUniversalTime(); var date = dateBegin.ToUniversalTime();
var query = this.db.Set<ChangeLog>() var query = db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator) .Where(e => e.IdDiscriminator == idDiscriminator)
.Where(e => e.Creation >= date || e.Obsolete >= date); .Where(e => e.Creation >= date || e.Obsolete >= date);
@ -233,7 +233,7 @@ public class ChangeLogRepository : IChangeLogRepository
.Select(group => new .Select(group => new
{ {
Min = group.Min(e => e.Creation), Min = group.Min(e => e.Creation),
Max = group.Max(e => (e.Obsolete.HasValue && e.Obsolete > e.Creation) Max = group.Max(e => e.Obsolete.HasValue && e.Obsolete > e.Creation
? e.Obsolete.Value ? e.Obsolete.Value
: e.Creation), : e.Creation),
}); });

View File

@ -0,0 +1,43 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Repositories;
public class SchemePropertyRepository : ISchemePropertyRepository
{
protected DbContext db;
public SchemePropertyRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<SchemeProperty> GetQueryReadOnly() => db.Set<SchemeProperty>();
public virtual async Task AddRange(DataSchemeDto dataSchemeDto, CancellationToken token)
{
var entities = dataSchemeDto.Select(e =>
KeyValuePair.Create(dataSchemeDto.DiscriminatorId, e)
.Adapt<SchemeProperty>()
);
await db.Set<SchemeProperty>().AddRangeAsync(entities, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entities = await query.ToArrayAsync(token);
DataSchemeDto? result = null;
if (entities.Length != 0)
{
var properties = entities.Select(e => e.Adapt<SchemePropertyDto>()).ToArray();
result = new DataSchemeDto(dataSchemeId, properties);
}
return result;
}
}

View File

@ -14,8 +14,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
this.db = db; this.db = db;
} }
protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>() protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>();
.Include(e => e.DataScheme);
public async virtual Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token) public async virtual Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{ {
@ -55,15 +54,6 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
.Select(g => KeyValuePair.Create(g.Key, g.OrderBy(i => i.Timestamp).Skip(skip).Take(take))); .Select(g => KeyValuePair.Create(g.Key, g.OrderBy(i => i.Timestamp).Skip(skip).Take(take)));
var entities = await groupQuery.ToArrayAsync(token); var entities = await groupQuery.ToArrayAsync(token);
//var root = new TVertex(
// OperationEnum.And,
// new TLeaf(OperationEnum.Equal, "A", 1),
// new TLeaf(OperationEnum.Equal, "B", 1)
//);
//var dataScheme = entities.First().Value.First().DataScheme;
//var specification = dataScheme.BuildFilter<TimestampedValues>(root);
//var que = ApplySpecification(specification);
var result = entities.ToDictionary(k => k.Key, v => v.Value.Select(e => ( var result = entities.ToDictionary(k => k.Key, v => v.Value.Select(e => (
e.Timestamp, e.Timestamp,
e.Values e.Values

View File

@ -1,21 +1,21 @@
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Database.Postgres.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using DD.Persistence.Database.Repositories;
namespace DD.Persistence.Database.Postgres.RepositoriesCached; namespace DD.Persistence.Database.RepositoriesCached;
public class DataSchemeCachedRepository : DataSchemeRepository public class SchemePropertyCachedRepository : SchemePropertyRepository
{ {
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
public DataSchemeCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db) public SchemePropertyCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
{ {
this.memoryCache = memoryCache; this.memoryCache = memoryCache;
} }
public override async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token) public override async Task AddRange(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{ {
await base.Add(dataSourceSystemDto, token); await base.AddRange(dataSourceSystemDto, token);
memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto); memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto);
} }

View File

@ -406,8 +406,12 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
private void Cleanup() private void Cleanup()
{ {
foreach (var item in discriminatorIds)
{
memoryCache.Remove(item);
}
discriminatorIds = []; discriminatorIds = [];
dbContext.CleanupDbSet<TimestampedValues>(); dbContext.CleanupDbSet<TimestampedValues>();
dbContext.CleanupDbSet<DataScheme>(); dbContext.CleanupDbSet<SchemeProperty>();
} }
} }

View File

@ -1,9 +1,11 @@
namespace DD.Persistence.Models; using System.Collections;
namespace DD.Persistence.Models;
/// <summary> /// <summary>
/// Схема для набора данных /// Схема для набора данных
/// </summary> /// </summary>
public class DataSchemeDto public class DataSchemeDto : IEnumerable<SchemePropertyDto>, IEquatable<IEnumerable<SchemePropertyDto>>
{ {
/// <summary> /// <summary>
/// Дискриминатор /// Дискриминатор
@ -11,12 +13,30 @@ public class DataSchemeDto
public Guid DiscriminatorId { get; set; } public Guid DiscriminatorId { get; set; }
/// <summary> /// <summary>
/// Наименования полей /// Поля
/// </summary> /// </summary>
public string[] PropNames { get; set; } = []; private IEnumerable<SchemePropertyDto> Properties { get; } = [];
/// <summary> /// <inheritdoc/>
/// Типы полей public DataSchemeDto(Guid discriminatorId, SchemePropertyDto[] Properties)
/// </summary> {
public PropTypeEnum[] PropTypes { get; set; } = []; DiscriminatorId = discriminatorId;
this.Properties = Properties;
}
/// <inheritdoc/>
public IEnumerator<SchemePropertyDto> GetEnumerator()
=> Properties.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public bool Equals(IEnumerable<SchemePropertyDto>? otherProperties)
{
//if (otherProperties is null)
// return false;
return Properties.SequenceEqual(otherProperties);
}
} }

View File

@ -1,14 +0,0 @@
namespace DD.Persistence.Models;
/// <summary>
/// Типы для набора данных
/// </summary>
public enum PropTypeEnum
{
/// <inheritdoc/>
String = 0,
/// <inheritdoc/>
Double = 1,
/// <inheritdoc/>
DateTime = 2
}

View File

@ -0,0 +1,30 @@
using System.Text.Json;
namespace DD.Persistence.Models;
/// <summary>
/// Индексируемого поле из схемы для набора данных
/// </summary>
public class SchemePropertyDto : IEquatable<SchemePropertyDto>
{
/// <summary>
/// Индекс поля
/// </summary>
public required int Index { get; set; }
/// <summary>
/// Наименование индексируемого поля
/// </summary>
public required string PropertyName { get; set; }
/// <summary>
/// Тип индексируемого поля
/// </summary>
public required JsonValueKind PropertyKind { get; set; }
/// <inheritdoc/>
public bool Equals(SchemePropertyDto? other)
{
return Index == other?.Index && PropertyName == other?.PropertyName && PropertyKind == other?.PropertyKind;
}
}

View File

@ -1,4 +1,4 @@
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Repositories; using DD.Persistence.Repositories;
using DD.Persistence.Services; using DD.Persistence.Services;
using NSubstitute; using NSubstitute;
@ -7,8 +7,8 @@ namespace DD.Persistence.Test;
public class TimestampedValuesServiceShould public class TimestampedValuesServiceShould
{ {
private readonly ITimestampedValuesRepository timestampedValuesRepository = Substitute.For<ITimestampedValuesRepository>(); private readonly ITimestampedValuesRepository timestampedValuesRepository = Substitute.For<ITimestampedValuesRepository>();
private readonly IDataSchemeRepository dataSchemeRepository = Substitute.For<IDataSchemeRepository>(); private readonly ISchemePropertyRepository dataSchemeRepository = Substitute.For<ISchemePropertyRepository>();
private readonly TimestampedValuesService timestampedValuesService; private TimestampedValuesService timestampedValuesService;
public TimestampedValuesServiceShould() public TimestampedValuesServiceShould()
{ {

View File

@ -5,7 +5,7 @@ namespace DD.Persistence.Repositories;
/// <summary> /// <summary>
/// Репозиторий для работы со схемами наборов данных /// Репозиторий для работы со схемами наборов данных
/// </summary> /// </summary>
public interface IDataSchemeRepository public interface ISchemePropertyRepository
{ {
/// <summary> /// <summary>
/// Добавить схему /// Добавить схему
@ -13,7 +13,7 @@ public interface IDataSchemeRepository
/// <param name="dataSourceSystemDto"></param> /// <param name="dataSourceSystemDto"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token); Task AddRange(DataSchemeDto dataSourceSystemDto, CancellationToken token);
/// <summary> /// <summary>
/// Вычитать схему /// Вычитать схему

View File

@ -2,6 +2,7 @@
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Repositories; using DD.Persistence.Repositories;
using DD.Persistence.Services.Interfaces; using DD.Persistence.Services.Interfaces;
using System.Text.Json;
namespace DD.Persistence.Services; namespace DD.Persistence.Services;
@ -9,10 +10,10 @@ namespace DD.Persistence.Services;
public class TimestampedValuesService : ITimestampedValuesService public class TimestampedValuesService : ITimestampedValuesService
{ {
private readonly ITimestampedValuesRepository timestampedValuesRepository; private readonly ITimestampedValuesRepository timestampedValuesRepository;
private readonly IDataSchemeRepository dataSchemeRepository; private readonly ISchemePropertyRepository dataSchemeRepository;
/// <inheritdoc/> /// <inheritdoc/>
public TimestampedValuesService(ITimestampedValuesRepository timestampedValuesRepository, IDataSchemeRepository relatedDataRepository) public TimestampedValuesService(ITimestampedValuesRepository timestampedValuesRepository, ISchemePropertyRepository relatedDataRepository)
{ {
this.timestampedValuesRepository = timestampedValuesRepository; this.timestampedValuesRepository = timestampedValuesRepository;
this.dataSchemeRepository = relatedDataRepository; this.dataSchemeRepository = relatedDataRepository;
@ -37,7 +38,7 @@ public class TimestampedValuesService : ITimestampedValuesService
{ {
var result = await timestampedValuesRepository.Get(discriminatorIds, geTimestamp, columnNames, skip, take, token); var result = await timestampedValuesRepository.Get(discriminatorIds, geTimestamp, columnNames, skip, take, token);
var dtos = await Materialize(result, token); var dtos = await BindingToDataScheme(result, token);
if (!columnNames.IsNullOrEmpty()) if (!columnNames.IsNullOrEmpty())
{ {
@ -52,9 +53,9 @@ public class TimestampedValuesService : ITimestampedValuesService
{ {
var result = await timestampedValuesRepository.GetFirst(discriminatorId, takeCount, token); var result = await timestampedValuesRepository.GetFirst(discriminatorId, takeCount, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) } var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary(); .ToDictionary();
var dtos = await Materialize(resultToMaterialize, token); var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos; return dtos;
} }
@ -64,9 +65,9 @@ public class TimestampedValuesService : ITimestampedValuesService
{ {
var result = await timestampedValuesRepository.GetLast(discriminatorId, takeCount, token); var result = await timestampedValuesRepository.GetLast(discriminatorId, takeCount, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) } var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary(); .ToDictionary();
var dtos = await Materialize(resultToMaterialize, token); var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos; return dtos;
} }
@ -81,9 +82,9 @@ public class TimestampedValuesService : ITimestampedValuesService
{ {
var result = await timestampedValuesRepository.GetResampledData(discriminatorId, beginTimestamp, intervalSec, approxPointsCount, token); var result = await timestampedValuesRepository.GetResampledData(discriminatorId, beginTimestamp, intervalSec, approxPointsCount, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) } var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary(); .ToDictionary();
var dtos = await Materialize(resultToMaterialize, token); var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos; return dtos;
} }
@ -93,41 +94,36 @@ public class TimestampedValuesService : ITimestampedValuesService
{ {
var result = await timestampedValuesRepository.GetGtDate(discriminatorId, beginTimestamp, token); var result = await timestampedValuesRepository.GetGtDate(discriminatorId, beginTimestamp, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) } var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary(); .ToDictionary();
var dtos = await Materialize(resultToMaterialize, token); var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos; return dtos;
} }
// ToDo: рефакторинг, переименовать (текущее название не отражает суть)
/// <summary> /// <summary>
/// Преобразовать результат запроса в набор dto /// Преобразовать результат запроса в набор dto
/// </summary> /// </summary>
/// <param name="queryResult"></param> /// <param name="queryResult"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
private async Task<IEnumerable<TimestampedValuesDto>> Materialize(IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> queryResult, CancellationToken token) private async Task<IEnumerable<TimestampedValuesDto>> BindingToDataScheme(IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> queryResult, CancellationToken token)
{ {
IEnumerable<TimestampedValuesDto> result = []; IEnumerable<TimestampedValuesDto> result = [];
foreach (var keyValuePair in queryResult) foreach (var keyValuePair in queryResult)
{ {
var dataScheme = await dataSchemeRepository.Get(keyValuePair.Key, token); var dataScheme = await dataSchemeRepository.Get(keyValuePair.Key, token);
if (dataScheme is null) if (dataScheme is null)
{
continue; continue;
}
foreach (var tuple in keyValuePair.Value) foreach (var tuple in keyValuePair.Value)
{ {
var identity = dataScheme!.PropNames;
var indexedIdentity = identity
.Select((value, index) => new { index, value });
var dto = new TimestampedValuesDto() var dto = new TimestampedValuesDto()
{ {
Timestamp = tuple.Timestamp.ToUniversalTime(), Timestamp = tuple.Timestamp.ToUniversalTime(),
Values = indexedIdentity Values = dataScheme
.ToDictionary(x => x.value, x => tuple.Values[x.index]) .ToDictionary(k => k.PropertyName, v => tuple.Values[v.Index])
}; };
result = result.Append(dto); result = result.Append(dto);
@ -147,60 +143,29 @@ public class TimestampedValuesService : ITimestampedValuesService
/// <exception cref="InvalidOperationException">Некорректный набор наименований полей</exception> /// <exception cref="InvalidOperationException">Некорректный набор наименований полей</exception>
private async Task CreateDataSchemeIfNotExist(Guid discriminatorId, TimestampedValuesDto dto, CancellationToken token) private async Task CreateDataSchemeIfNotExist(Guid discriminatorId, TimestampedValuesDto dto, CancellationToken token)
{ {
var propNames = dto.Values.Keys.ToArray(); var valuesList = dto.Values.ToList();
var propTypes = GetPropTypes(dto); var properties = valuesList.ToList().Select(e => new SchemePropertyDto() {
Index = valuesList.IndexOf(e),
PropertyName = e.Key,
PropertyKind = ((JsonElement) e.Value).ValueKind
}).ToArray();
var dataScheme = await dataSchemeRepository.Get(discriminatorId, token); var dataScheme = await dataSchemeRepository.Get(discriminatorId, token);
if (dataScheme is null) if (dataScheme is null)
{ {
dataScheme = new DataSchemeDto() dataScheme = new DataSchemeDto(discriminatorId, properties);
{ await dataSchemeRepository.AddRange(dataScheme, token);
DiscriminatorId = discriminatorId,
PropNames = propNames,
PropTypes = propTypes
};
await dataSchemeRepository.Add(dataScheme, token);
return; return;
} }
if (!dataScheme.PropNames.SequenceEqual(propNames)) if (!dataScheme.Equals(properties))
{ {
var expectedFieldNames = string.Join(", ", dataScheme.PropNames);
var actualFieldNames = string.Join(", ", propNames);
throw new InvalidOperationException($"Для системы {discriminatorId.ToString()} " + throw new InvalidOperationException($"Для системы {discriminatorId.ToString()} " +
$"характерен набор данных: [{expectedFieldNames}], однако был передан набор: [{actualFieldNames}]"); $"был передан нехарактерный набор данных");
} }
} }
/// <summary>
/// Получить типы для набора данных в соответствии с индексацией
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
private PropTypeEnum[] GetPropTypes(TimestampedValuesDto dto)
{
var types = dto.Values.Select(e =>
{
var valueString = e.Value.ToString();
if (valueString is null)
throw new ArgumentNullException("Переданный набор данных содержит null, в следствии чего не удаётся определить типы полей");
if (DateTimeOffset.TryParse(valueString, out _))
return PropTypeEnum.DateTime;
var doubleString = valueString.Replace('.', ',');
if (double.TryParse(doubleString, out _))
return PropTypeEnum.Double;
return PropTypeEnum.String;
});
return types.ToArray();
}
/// <summary> /// <summary>
/// Отсеить лишние поля в соответствии с заданным фильтром /// Отсеить лишние поля в соответствии с заданным фильтром
/// </summary> /// </summary>