From 8f4ad21a3ca66f79d5f11ac1d37dec58bc85a030 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Mon, 16 Mar 2026 16:21:16 +1000 Subject: [PATCH] =?UTF-8?q?fix(dal):=20UnitFilterServicen=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B8=D0=BD?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B2=D0=BD=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=AD=D0=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/UnitFilterServiceOptions.cs | 2 +- .../UnitFilterService/UnitFilterService.cs | 264 +++++++++--------- PARR.DAL/ParrDalInstaller.cs | 2 +- 3 files changed, 139 insertions(+), 129 deletions(-) diff --git a/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs b/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs index 23926c1f..3c94f0c0 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs @@ -2,6 +2,6 @@ { internal class UnitFilterServiceOptions { - public int LoadBatchSize { get; set; } = 100; + public int LoadBatchSize { get; set; } = 50; } } diff --git a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs index 84aa80da..be4acf49 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs @@ -7,6 +7,7 @@ using PARR.DAL.Contracts; using PARR.DAL.DomainServices.UnitFilterService.Models; using PARR.DAL.Models.Job; using PARR.DAL.Models.Unit; +using PARR.DAL.Services.Implementations.Unit; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; using System.Diagnostics; @@ -26,6 +27,7 @@ internal class UnitFilterService : IUnitFilterService private readonly IUnitService unitService; private readonly IUnitInUnitService unitInUnitService; private readonly IRedisCacheService cacheService; + private readonly IUnitFieldService unitFieldService; private readonly IUnitInValueService unitInValueService; public UnitFilterService( @@ -35,7 +37,8 @@ internal class UnitFilterService : IUnitFilterService IUnitInUnitService unitInUnitService, IUnitInValueService unitInValueService, IRedisCacheService cacheService, - IOptions options + IOptions options, + IUnitFieldService unitFieldService ) { this.logger = logger; @@ -43,6 +46,7 @@ internal class UnitFilterService : IUnitFilterService this.unitService = unitService; this.unitInUnitService = unitInUnitService; this.cacheService = cacheService; + this.unitFieldService = unitFieldService; this.unitInValueService = unitInValueService; batchSize = options.Value.LoadBatchSize; @@ -138,7 +142,7 @@ internal class UnitFilterService : IUnitFilterService // 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ FilteredUnitContext var relStopwatch = Stopwatch.StartNew(); - var relationshipFilteredContexts = await ApplyRelationshipFiltersOnDbAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken); + var relationshipFilteredContexts = await ProcessRelationshipFiltersAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken); relStopwatch.Stop(); if (!relationshipFilteredContexts.Any()) @@ -248,7 +252,7 @@ internal class UnitFilterService : IUnitFilterService { return await jobService .Get().AsNoTracking() - .AsSplitQuery() + .AsSplitQuery() .Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters).ThenInclude(ff => ff.UnitField) .Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters).ThenInclude(rf => rf.UnitField) .Include(j => j.Group).ThenInclude(g => g!.GroupType) @@ -306,22 +310,14 @@ internal class UnitFilterService : IUnitFilterService { query = query.Where(u => !u.UnitValues.Any(v => v.FieldId == fieldFilter.FieldId && - EF.Functions.Like(v.Value.Value, dbValueMask))); + EF.Functions.ILike(v.Value.Value, dbValueMask))); } else { query = query.Where(u => u.UnitValues.Any(v => v.FieldId == fieldFilter.FieldId && - EF.Functions.Like(v.Value.Value, dbValueMask))); + EF.Functions.ILike(v.Value.Value, dbValueMask))); } - - // Выполняем промежуточный запрос для логирования - //var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken); - //logger.LogDebug(" После фильтра #{Index}: осталось {Count} юнитов", filterIndex, intermediateResult.Count); - - //// Обновляем query для следующей итерации - //query = unitService.Get().AsNoTracking() - // .Where(u => intermediateResult.Contains(u.Id)); } var result = await query.Select(u => u.Id).ToListAsync(cancellationToken); @@ -337,7 +333,7 @@ internal class UnitFilterService : IUnitFilterService /// /// /// - private async Task> ApplyRelationshipFiltersOnDbAsync( + private async Task> ProcessRelationshipFiltersAsync( List unitIds, IEnumerable relationshipFilters, CancellationToken cancellationToken = default) @@ -360,9 +356,9 @@ internal class UnitFilterService : IUnitFilterService // Обработка родительских фильтров if (parentRelFilters.Any()) { - await ApplyRelationshipFiltersAsync( + await ApplyRelationshipFiltersOnDbAsync( unitIds, - resultContexts, // ← Передаём словарь, а не список + resultContexts, parentRelFilters, isParentDirection: true, cancellationToken); @@ -371,9 +367,9 @@ internal class UnitFilterService : IUnitFilterService // Обработка дочерних фильтров if (childRelFilters.Any()) { - await ApplyRelationshipFiltersAsync( + await ApplyRelationshipFiltersOnDbAsync( unitIds, - resultContexts, // ← Передаём словарь, а не список + resultContexts, childRelFilters, isParentDirection: false, cancellationToken); @@ -392,85 +388,100 @@ internal class UnitFilterService : IUnitFilterService result.Count, resultContexts.Count); #if DEBUG + // Собираем все UnitId для пакетной загрузки (основные + родители + дети) + var allUnitIds = result.Select(c => c.UnitId) + .Concat(result.SelectMany(c => c.ValidParentIds)) + .Concat(result.SelectMany(c => c.ValidChildIds)) + .Distinct() + .Take(100) + .ToList(); + + // Пакетная загрузка всех юнитов + var unitsMap = await unitService.Get().AsNoTracking() + .Where(u => allUnitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, cancellationToken); + + // Получаем все FieldId из фильтров для отображения значений + var parentFieldIds = relationshipFilters.Where(rf => rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList(); + var childFieldIds = relationshipFilters.Where(rf => !rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList(); + var allFieldIds = parentFieldIds.Concat(childFieldIds).Distinct().ToList(); + + // ПАКЕТНАЯ загрузка значений для ВСЕХ юнитов сразу (1 запрос вместо N+1) + var allValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitIds, allFieldIds); + var valuesByUnit = allValues.GroupBy(v => v.UnitId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // ПАКЕТНАЯ загрузка имён полей (1 запрос вместо N+1) + var fieldsMap = await unitFieldService.Get() + .AsNoTracking() + .Where(f => allFieldIds.Contains(f.Id)) + .ToDictionaryAsync(f => f.Id, f => f.AihitName, cancellationToken); + foreach (var context in result.Take(10)) { - var u = await unitService.GetAsync(context.UnitId); + var u = unitsMap.GetValueOrDefault(context.UnitId); if (u == null) continue; - logger.LogDebug("📦 Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id); - - // Получаем все FieldId из фильтров для отображения значений - var parentFieldIds = relationshipFilters.Where(rf => rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList(); - var childFieldIds = relationshipFilters.Where(rf => !rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList(); + logger.LogDebug("Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id); // === РОДИТЕЛИ === if (context.ValidParentIds.Any()) { - logger.LogDebug("\t📌 Фильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); + logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); foreach (var parentId in context.ValidParentIds) { - var p = await unitService.GetAsync(parentId); - logger.LogDebug("\t\t👤 {ParentName} (Id={ParentId})", p?.Name, parentId); + var p = unitsMap.GetValueOrDefault(parentId); + logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name, parentId); - // Выводим значения только для полей из RelationshipFilters - if (parentFieldIds.Any()) + // Берём значения из кэша, а не делаем запрос + if (parentFieldIds.Any() && valuesByUnit.TryGetValue(parentId, out var parentValues)) { - var parentValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync( - new[] { parentId }, - parentFieldIds - ); - - foreach (var pv in parentValues) + foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId))) { - var field = pv.Field; - var value = pv.Value?.Value; - logger.LogDebug("\t\t 🔹 {FieldName} = {FieldValue}", - field?.AihitName ?? $"FieldId={pv.FieldId}", - value ?? "null"); + // Используем предварительно загруженный словарь полей + var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}"; + logger.LogDebug("\t\t {FieldName} = {FieldValue}", + fieldName, + pv.Value?.Value ?? "null"); } } } } else if (parentRelFilters.Any()) { - logger.LogDebug("\t Родительские фильтры заданы, но подходящих родителей не найдено"); + logger.LogDebug("\tРодительские фильтры заданы, но подходящих родителей не найдено"); } // === ДЕТИ === if (context.ValidChildIds.Any()) { - logger.LogDebug("\t Фильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); + logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); foreach (var childId in context.ValidChildIds) { - var c = await unitService.GetAsync(childId); - logger.LogDebug("\t\t {ChildName} (Id={ChildId})", c?.Name, childId); + var c = unitsMap.GetValueOrDefault(childId); + logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name, childId); - // Выводим значения только для полей из RelationshipFilters - if (childFieldIds.Any()) + // Берём значения из кэша, а не делаем запрос + if (childFieldIds.Any() && valuesByUnit.TryGetValue(childId, out var childValues)) { - var childValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync( - new[] { childId }, - childFieldIds - ); - - foreach (var cv in childValues) + foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId))) { - var field = cv.Field; - var value = cv.Value?.Value; - logger.LogDebug("\t\t {FieldName} = {FieldValue}", - field?.AihitName ?? $"FieldId={cv.FieldId}", - value ?? "null"); + // Используем предварительно загруженный словарь полей + var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}"; + logger.LogDebug("\t\t {FieldName} = {FieldValue}", + fieldName, + cv.Value?.Value ?? "null"); } } } } else if (childRelFilters.Any()) { - logger.LogDebug("\t Дочерние фильтры заданы, но подходящих детей не найдено"); + logger.LogDebug("\tДочерние фильтры заданы, но подходящих детей не найдено"); } } #endif @@ -733,57 +744,11 @@ internal class UnitFilterService : IUnitFilterService } #region вспомогательные методы - /// - /// Оценивает, проходит ли юнит фильтр связей - /// - private bool EvaluateFilter( - List unitRelations, // Все связи юнита - List matchingRelations, // Связи, которые соответствуют маске - bool isFullMatch, // Полное совпадение (All) или хотя бы одно (Any) - bool isInverse, // Обратный фильтр - bool hasRelations // Есть ли связи у юнита - ) - { - int matchingCount = unitRelations.Count(r => matchingRelations.Contains(r)); - int totalCount = unitRelations.Count; - - // Случай: нет связей в направлении - if (!hasRelations) - { - // Если IsInverse = true → юнит проходит (нет связей, которые нарушают) - // Если IsInverse = false → юнит не проходит (нет связей, которые соответствуют) - return isInverse; - } - - // Основная логика — только для определения, проходит ли юнит - if (isFullMatch) - { - // ВСЕ связи должны соответствовать - bool allMatch = matchingCount == totalCount; - - if (isInverse) - return !allMatch; // НЕ все связи соответствуют - else - return allMatch; // ВСЕ связи соответствуют - } - else - { - // ХОТЯ БЫ ОДНА связь должна соответствовать - bool anyMatch = matchingCount > 0; - - if (isInverse) - return !anyMatch; // НИ ОДНА связь не соответствует - else - return anyMatch; // ХОТЯ БЫ ОДНА связь соответствует - } - } - - /// /// Применяет фильтры к связям (родителям или детям) /// Связь должна пройти ВСЕ фильтры направления /// - private async Task ApplyRelationshipFiltersAsync( + private async Task ApplyRelationshipFiltersOnDbAsync( List unitIds, Dictionary resultContexts, List relFilters, @@ -820,7 +785,6 @@ internal class UnitFilterService : IUnitFilterService .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); // Находим связи, которые проходят ВСЕ фильтры одновременно - // Для каждого TargetId считаем, сколько фильтров он прошёл var targetFilterPassCount = new Dictionary(); foreach (var relFilter in relFilters) @@ -847,13 +811,13 @@ internal class UnitFilterService : IUnitFilterService isParentDirection ? "Parent" : "Child", fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); - // Находим целевые юниты, которые соответствуют текущему фильтру + // Находим целевые юниты, которые соответствуют ТЕКУЩЕМУ фильтру var matchingTargetIds = await unitService.Get() .AsNoTracking() .Where(u => allTargetIds.Contains(u.Id)) .Where(u => u.UnitValues.Any(v => v.FieldId == relFilter.FieldId && - EF.Functions.Like(v.Value.Value, dbValueMask))) + EF.Functions.ILike(v.Value.Value, dbValueMask))) .Select(u => u.Id) .ToListAsync(cancellationToken); @@ -862,15 +826,36 @@ internal class UnitFilterService : IUnitFilterService isParentDirection ? "родителей" : "детей"); // Считаем, сколько фильтров прошёл каждый TargetId - foreach (var targetId in matchingTargetIds) + if (relFilter.IsInverse) { - if (!targetFilterPassCount.ContainsKey(targetId)) - targetFilterPassCount[targetId] = 0; - targetFilterPassCount[targetId]++; + // Для IsInverse: "проходит" фильтр тот, кто НЕ соответствует маске + foreach (var targetId in allTargetIds) + { + if (!matchingTargetIds.Contains(targetId)) + { + if (!targetFilterPassCount.ContainsKey(targetId)) + targetFilterPassCount[targetId] = 0; + targetFilterPassCount[targetId]++; + } + } + + logger.LogDebug(" Для IsInverse: {NonMatchCount} {TargetType} НЕ соответствуют маске", + allTargetIds.Count - matchingTargetIds.Count, + isParentDirection ? "родителей" : "детей"); + } + else + { + // Для обычного фильтра: "проходит" тот, кто соответствует маске + foreach (var targetId in matchingTargetIds) + { + if (!targetFilterPassCount.ContainsKey(targetId)) + targetFilterPassCount[targetId] = 0; + targetFilterPassCount[targetId]++; + } } } - // ✅ Оставляем только те связи, которые прошли ВСЕ фильтры + // Оставляем только те связи, которые прошли ВСЕ фильтры int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim())); var validTargetIds = targetFilterPassCount .Where(kvp => kvp.Value == requiredFiltersCount) @@ -898,22 +883,47 @@ internal class UnitFilterService : IUnitFilterService var validTargets = unitTargets.Intersect(validTargetIds).ToList(); bool hasValidTargets = validTargets.Any(); - // Оцениваем, проходит ли юнит фильтр - bool passesFilter = EvaluateFilter( - validTargets, // Только связи, прошедшие ВСЕ фильтры - validTargets, // matchingRelations = validTargets (все валидные) - relFilters.All(rf => rf.IsFullMatch), // IsFullMatch для всех фильтров - relFilters.Any(rf => rf.IsInverse), // IsInverse для всех фильтров - hasTargets - ); + // Логика с IsFullMatch + bool isFullMatch = relFilters.All(rf => rf.IsFullMatch); + bool isInverse = relFilters.Any(rf => rf.IsInverse); - if (passesFilter && hasValidTargets) + bool passesFilter; + + if (!hasTargets) { - // Сохраняем только валидные связи - if (isParentDirection) - context.ValidParentIds.UnionWith(validTargets); + // Нет связей вообще + passesFilter = isInverse; // IsInverse=true → проходит, иначе нет + } + else if (isFullMatch) + { + // ВСЕ связи должны быть валидными + passesFilter = validTargets.Count == unitTargets.Count; + } + else + { + // ХОТЯ БЫ ОДНА связь должна быть валидной + passesFilter = hasValidTargets; + } + + if (passesFilter) + { + // Сохраняем связи + if (isFullMatch) + { + // Для IsFullMatch=true: сохраняем ВСЕ связи (они все валидные) + if (isParentDirection) + context.ValidParentIds.UnionWith(unitTargets); + else + context.ValidChildIds.UnionWith(unitTargets); + } else - context.ValidChildIds.UnionWith(validTargets); + { + // Для IsFullMatch=false: сохраняем ТОЛЬКО валидные связи + if (isParentDirection) + context.ValidParentIds.UnionWith(validTargets); + else + context.ValidChildIds.UnionWith(validTargets); + } } else { @@ -951,7 +961,7 @@ internal class UnitFilterService : IUnitFilterService } var initialUnitIds = await unitService.Get().AsNoTracking() - .Where(unit => EF.Functions.Like(unit.Name, filter.UnitFilter)) + .Where(unit => EF.Functions.ILike(unit.Name, filter.UnitFilter)) .Select(u => u.Id) .ToListAsync(cancellationToken); diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index eb5c539e..5a92befb 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -151,7 +151,7 @@ namespace PARR.DAL services.AddTransient(); services.AddTransient(); - services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 1000 }); + services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 50 }); #endregion