From acc9e6494a9df663deb0843a7f35394962857cdb Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Tue, 26 Nov 2024 10:23:48 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D1=85=D0=BD=D0=BE=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D1=85=20=D1=81=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/TechMessagesController.cs | 12 +- ...126044756_TechMessageMigration.Designer.cs | 175 ++++++++++++ .../20241126044756_TechMessageMigration.cs | 59 ++++ .../PersistenceDbContextModelSnapshot.cs | 38 ++- Persistence.Repository/DependencyInjection.cs | 1 + .../Extensions/EFExtensionsSortBy.cs | 267 ++++++++++++++++++ .../Repositories/TechMessagesRepository.cs | 19 ++ .../Repositories/ISetpointRepository.cs | 14 +- .../Repositories/ITechMessagesRepository.cs | 8 + 9 files changed, 582 insertions(+), 11 deletions(-) create mode 100644 Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.Designer.cs create mode 100644 Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.cs create mode 100644 Persistence.Repository/Extensions/EFExtensionsSortBy.cs diff --git a/Persistence.API/Controllers/TechMessagesController.cs b/Persistence.API/Controllers/TechMessagesController.cs index 0eb183d..16db89c 100644 --- a/Persistence.API/Controllers/TechMessagesController.cs +++ b/Persistence.API/Controllers/TechMessagesController.cs @@ -15,11 +15,15 @@ namespace Persistence.API.Controllers this.techMessagesRepository = techMessagesRepository; } - public Task>> GetPage(RequestDto request, CancellationToken token) + [HttpGet] + public async Task>> GetPage([FromQuery] RequestDto request, CancellationToken token) { - throw new NotImplementedException(); + var result = await techMessagesRepository.GetPage(request, token); + + return Ok(result); } + [HttpGet("statistics")] public async Task> GetStatistics(int importantId, string autoDrillingSystem, CancellationToken token) { var result = await techMessagesRepository.GetStatistics(importantId, autoDrillingSystem, token); @@ -27,6 +31,7 @@ namespace Persistence.API.Controllers return Ok(result); } + [HttpGet("systems")] public async Task>> GetSystems(CancellationToken token) { var result = await techMessagesRepository.GetSystems(token); @@ -34,7 +39,8 @@ namespace Persistence.API.Controllers return Ok(result); } - public async Task> InsertRange(IEnumerable dtos, CancellationToken token) + [HttpPost] + public async Task> InsertRange([FromBody] IEnumerable dtos, CancellationToken token) { var result = await techMessagesRepository.InsertRange(dtos, token); diff --git a/Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.Designer.cs b/Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.Designer.cs new file mode 100644 index 0000000..5d6817e --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.Designer.cs @@ -0,0 +1,175 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Persistence.Database.Model; + +#nullable disable + +namespace Persistence.Database.Postgres.Migrations +{ + [DbContext(typeof(PersistenceDbContext))] + [Migration("20241126044756_TechMessageMigration")] + partial class TechMessageMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("Russian_Russia.1251") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Id события"); + + b.Property("AutoDrillingSystem") + .HasColumnType("varchar(256)") + .HasComment("Система автобурения, к которой относится сообщение"); + + b.Property("Depth") + .HasColumnType("double precision") + .HasComment("Глубина забоя"); + + b.Property("ImportantId") + .HasColumnType("integer") + .HasComment("Id Категории важности"); + + b.Property("MessageText") + .HasColumnType("varchar(512)") + .HasComment("Текст сообщения"); + + b.Property("OccurrenceDate") + .HasColumnType("timestamp with time zone") + .HasComment("Дата возникновения"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("Id пользователя за пультом бурильщика"); + + b.HasKey("EventId"); + + b.ToTable("TechMessage"); + }); + + modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => + { + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("AxialLoad") + .HasColumnType("double precision") + .HasColumnName("axialLoad"); + + b.Property("BitDepth") + .HasColumnType("double precision") + .HasColumnName("bitDepth"); + + b.Property("BlockPosition") + .HasColumnType("double precision") + .HasColumnName("blockPosition"); + + b.Property("BlockSpeed") + .HasColumnType("double precision") + .HasColumnName("blockSpeed"); + + b.Property("Flow") + .HasColumnType("double precision") + .HasColumnName("flow"); + + b.Property("HookWeight") + .HasColumnType("double precision") + .HasColumnName("hookWeight"); + + b.Property("IdFeedRegulator") + .HasColumnType("integer") + .HasColumnName("idFeedRegulator"); + + b.Property("Mode") + .HasColumnType("integer") + .HasColumnName("mode"); + + b.Property("Mse") + .HasColumnType("double precision") + .HasColumnName("mse"); + + b.Property("MseState") + .HasColumnType("smallint") + .HasColumnName("mseState"); + + b.Property("Pressure") + .HasColumnType("double precision") + .HasColumnName("pressure"); + + b.Property("Pump0Flow") + .HasColumnType("double precision") + .HasColumnName("pump0Flow"); + + b.Property("Pump1Flow") + .HasColumnType("double precision") + .HasColumnName("pump1Flow"); + + b.Property("Pump2Flow") + .HasColumnType("double precision") + .HasColumnName("pump2Flow"); + + b.Property("RotorSpeed") + .HasColumnType("double precision") + .HasColumnName("rotorSpeed"); + + b.Property("RotorTorque") + .HasColumnType("double precision") + .HasColumnName("rotorTorque"); + + b.Property("User") + .HasColumnType("text") + .HasColumnName("user"); + + b.Property("WellDepth") + .HasColumnType("double precision") + .HasColumnName("wellDepth"); + + b.HasKey("Date"); + + b.ToTable("DataSaub"); + }); + + modelBuilder.Entity("Persistence.Database.Model.Setpoint", b => + { + b.Property("Key") + .HasColumnType("uuid") + .HasComment("Ключ"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasComment("Дата создания уставки"); + + b.Property("IdUser") + .HasColumnType("integer") + .HasComment("Id автора последнего изменения"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Значение уставки"); + + b.HasKey("Key", "Created"); + + b.ToTable("Setpoint"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.cs b/Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.cs new file mode 100644 index 0000000..46a71a0 --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241126044756_TechMessageMigration.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Database.Postgres.Migrations +{ + /// + public partial class TechMessageMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Created", + table: "Setpoint", + type: "timestamp with time zone", + nullable: false, + comment: "Дата создания уставки", + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldComment: "Дата изменения уставки"); + + migrationBuilder.CreateTable( + name: "TechMessage", + columns: table => new + { + EventId = table.Column(type: "uuid", nullable: false, comment: "Id события"), + ImportantId = table.Column(type: "integer", nullable: false, comment: "Id Категории важности"), + OccurrenceDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "Дата возникновения"), + Depth = table.Column(type: "double precision", nullable: true, comment: "Глубина забоя"), + MessageText = table.Column(type: "varchar(512)", nullable: true, comment: "Текст сообщения"), + AutoDrillingSystem = table.Column(type: "varchar(256)", nullable: true, comment: "Система автобурения, к которой относится сообщение"), + UserId = table.Column(type: "uuid", nullable: false, comment: "Id пользователя за пультом бурильщика") + }, + constraints: table => + { + table.PrimaryKey("PK_TechMessage", x => x.EventId); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TechMessage"); + + migrationBuilder.AlterColumn( + name: "Created", + table: "Setpoint", + type: "timestamp with time zone", + nullable: false, + comment: "Дата изменения уставки", + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldComment: "Дата создания уставки"); + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs index f41f669..4446a1c 100644 --- a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs +++ b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs @@ -24,6 +24,42 @@ namespace Persistence.Database.Postgres.Migrations NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Id события"); + + b.Property("AutoDrillingSystem") + .HasColumnType("varchar(256)") + .HasComment("Система автобурения, к которой относится сообщение"); + + b.Property("Depth") + .HasColumnType("double precision") + .HasComment("Глубина забоя"); + + b.Property("ImportantId") + .HasColumnType("integer") + .HasComment("Id Категории важности"); + + b.Property("MessageText") + .HasColumnType("varchar(512)") + .HasComment("Текст сообщения"); + + b.Property("OccurrenceDate") + .HasColumnType("timestamp with time zone") + .HasComment("Дата возникновения"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("Id пользователя за пультом бурильщика"); + + b.HasKey("EventId"); + + b.ToTable("TechMessage"); + }); + modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => { b.Property("Date") @@ -115,7 +151,7 @@ namespace Persistence.Database.Postgres.Migrations b.Property("Created") .HasColumnType("timestamp with time zone") - .HasComment("Дата изменения уставки"); + .HasComment("Дата создания уставки"); b.Property("IdUser") .HasColumnType("integer") diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index 27063cb..a8a08cc 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -17,6 +17,7 @@ public static class DependencyInjection services.AddTransient, TimeSeriesDataRepository>(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/Persistence.Repository/Extensions/EFExtensionsSortBy.cs b/Persistence.Repository/Extensions/EFExtensionsSortBy.cs new file mode 100644 index 0000000..03b9c65 --- /dev/null +++ b/Persistence.Repository/Extensions/EFExtensionsSortBy.cs @@ -0,0 +1,267 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +namespace Persistence.Repository.Extensions; + +public static class EFExtensionsSortBy +{ + struct TypeAccessor + { + public LambdaExpression KeySelector { get; set; } + public MethodInfo OrderBy { get; set; } + public MethodInfo OrderByDescending { get; set; } + public MethodInfo ThenBy { get; set; } + public MethodInfo ThenByDescending { get; set; } + } + + private static ConcurrentDictionary> TypePropSelectors { get; set; } = + new(); + + private static readonly MethodInfo methodOrderBy = GetExtOrderMethod("OrderBy"); + + private static readonly MethodInfo methodOrderByDescending = GetExtOrderMethod("OrderByDescending"); + + private static readonly MethodInfo methodThenBy = GetExtOrderMethod("ThenBy"); + + private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending"); + + private static MethodInfo GetExtOrderMethod(string methodName) + => typeof(Queryable) + .GetMethods() + .Where(m => m.Name == methodName && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[1].ParameterType.IsAssignableTo(typeof(LambdaExpression))) + .Single(); + + private static Dictionary MakeTypeAccessors(Type type) + { + var propContainer = new Dictionary(); + var properties = type.GetProperties(); + foreach (var propertyInfo in properties) + { + var name = propertyInfo.Name.ToLower(); + ParameterExpression arg = Expression.Parameter(type, "x"); + MemberExpression property = Expression.Property(arg, propertyInfo.Name); + var selector = Expression.Lambda(property, new ParameterExpression[] { arg }); + var typeAccessor = new TypeAccessor + { + KeySelector = selector, + OrderBy = methodOrderBy.MakeGenericMethod(type, propertyInfo.PropertyType), + OrderByDescending = methodOrderByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), + ThenBy = methodThenBy.MakeGenericMethod(type, propertyInfo.PropertyType), + ThenByDescending = methodThenByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), + }; + + propContainer.Add(name, typeAccessor); + } + + return propContainer; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию. + /// + /// + /// + /// + /// Свойство сортировки. + /// Состоит из названия свойства (в любом регистре) + /// и опционально указания направления сортировки "asc" или "desc" + /// + /// + /// var query = query("Date desc"); + /// + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + IEnumerable propertySorts) + { + if (propertySorts?.Any() != true) + return (IOrderedQueryable)query; + + var sortEnum = propertySorts.GetEnumerator(); + sortEnum.MoveNext(); + var orderedQuery = query.SortBy(sortEnum.Current); + + while (sortEnum.MoveNext()) + orderedQuery = orderedQuery.ThenSortBy(sortEnum.Current); + + return orderedQuery; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию. + /// Этот метод сбросит ранее наложенные сортировки. + /// + /// + /// + /// + /// Свойство сортировки. + /// Состоит из названия свойства (в любом регистре) + /// и опционально указания направления сортировки "asc" или "desc" + /// + /// + /// var query = query("Date desc"); + /// + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + string propertySort) + { + var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); + var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; + var propertyName = parts[0]; + + var newQuery = query.SortBy(propertyName, isDesc); + return newQuery; + } + + /// + /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию. + /// + /// + /// + /// + /// Свойство сортировки. + /// Состоит из названия свойства (в любом регистре) + /// и опционально указания направления сортировки "asc" или "desc" + /// + /// + /// var query = query("Date desc"); + /// + /// Запрос с примененной сортировкой + public static IOrderedQueryable ThenSortBy( + this IOrderedQueryable query, + string propertySort) + { + var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); + var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; + var propertyName = parts[0]; + + var newQuery = query.ThenSortBy(propertyName, isDesc); + return newQuery; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию + /// + /// + /// + /// Название свойства (в любом регистре) + /// Сортировать по убыванию + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + string propertyName, + bool isDesc) + { + Type rootType = typeof(TSource); + var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); + var propertyNameLower = propertyName.ToLower(); + + MethodInfo orderByDescending; + MethodInfo orderByAscending; + + LambdaExpression? lambdaExpression = null; + + if (propertyName.Contains('.')) + { + Type type = rootType; + ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); + Expression expr = rootExpression; + + var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < propertyPath.Length; i++) + { + PropertyInfo pi = type.GetProperty(propertyPath[i])!; + expr = Expression.Property(expr, pi); + type = pi.PropertyType; + } + + Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); + lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); + + orderByAscending = methodOrderBy.MakeGenericMethod(rootType, type); + orderByDescending = methodOrderByDescending.MakeGenericMethod(rootType, type); + } + else + { + var rootTypeAccessor = typePropSelector[propertyNameLower]; + orderByAscending = rootTypeAccessor.OrderBy; + orderByDescending = rootTypeAccessor.OrderByDescending; + lambdaExpression = rootTypeAccessor.KeySelector; + } + + var genericMethod = isDesc + ? orderByDescending + : orderByAscending; + + var newQuery = (IOrderedQueryable)genericMethod + .Invoke(genericMethod, new object[] { query, lambdaExpression })!; + return newQuery; + } + + /// + /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию + /// + /// + /// + /// Название свойства (в любом регистре) + /// Сортировать по убыванию + /// Запрос с примененной сортировкой + public static IOrderedQueryable ThenSortBy( + this IOrderedQueryable query, + string propertyName, + bool isDesc) + { + Type rootType = typeof(TSource); + var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); + var propertyNameLower = propertyName.ToLower(); + + MethodInfo orderByDescending; + MethodInfo orderByAscending; + + LambdaExpression? lambdaExpression = null; + + // TODO: Устранить дублирование кода + if (propertyName.Contains('.')) + { + Type type = rootType; + ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); + Expression expr = rootExpression; + + var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < propertyPath.Length; i++) + { + PropertyInfo pi = type.GetProperty(propertyPath[i])!; + expr = Expression.Property(expr, pi); + type = pi.PropertyType; + } + + Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); + lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); + + orderByAscending = methodThenBy.MakeGenericMethod(rootType, type); + orderByDescending = methodThenByDescending.MakeGenericMethod(rootType, type); + } + else + { + var rootTypeAccessor = typePropSelector[propertyNameLower]; + orderByAscending = rootTypeAccessor.ThenBy; + orderByDescending = rootTypeAccessor.ThenByDescending; + lambdaExpression = rootTypeAccessor.KeySelector; + } + + var genericMethod = isDesc + ? orderByDescending + : orderByAscending; + + var newQuery = (IOrderedQueryable)genericMethod + .Invoke(genericMethod, new object[] { query, lambdaExpression })!; + return newQuery; + } +} \ No newline at end of file diff --git a/Persistence.Repository/Repositories/TechMessagesRepository.cs b/Persistence.Repository/Repositories/TechMessagesRepository.cs index e7a37bc..f5ddb35 100644 --- a/Persistence.Repository/Repositories/TechMessagesRepository.cs +++ b/Persistence.Repository/Repositories/TechMessagesRepository.cs @@ -16,6 +16,25 @@ namespace Persistence.Repository.Repositories protected virtual IQueryable GetQueryReadOnly() => db.Set(); + public async Task> GetPage(RequestDto request, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .SortBy(request.SortSettings) + .Skip(request.Skip) + .Take(request.Take) + .ToListAsync(); + var dto = new PaginationContainer() + { + Skip = request.Skip, + Take = request.Take, + Count = entities.Count, + Items = entities.Select(e => e.Adapt()) + }; + + return dto; + } + public async Task GetStatistics(int importantId, string autoDrillingSystem, CancellationToken token) { var query = GetQueryReadOnly(); diff --git a/Persistence/Repositories/ISetpointRepository.cs b/Persistence/Repositories/ISetpointRepository.cs index 1d82b16..db6838c 100644 --- a/Persistence/Repositories/ISetpointRepository.cs +++ b/Persistence/Repositories/ISetpointRepository.cs @@ -7,13 +7,13 @@ namespace Persistence.Repositories; /// public interface ISetpointRepository { - /// - /// Получить значения уставок по набору ключей - /// - /// - /// - /// - Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); + /// + /// Получить значения уставок по набору ключей + /// + /// + /// + /// + Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); /// /// Получить значения уставок за определенный момент времени diff --git a/Persistence/Repositories/ITechMessagesRepository.cs b/Persistence/Repositories/ITechMessagesRepository.cs index c681184..1be90e9 100644 --- a/Persistence/Repositories/ITechMessagesRepository.cs +++ b/Persistence/Repositories/ITechMessagesRepository.cs @@ -7,6 +7,14 @@ namespace Persistence.Repositories /// public interface ITechMessagesRepository { + /// + /// Получить страницу списка объектов + /// + /// + /// + /// + Task> GetPage(RequestDto request, CancellationToken token); + /// /// Добавление новых сообщений ///