From 3704a131b4abec6edac375197184342a8808f86a Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Tue, 17 Mar 2026 13:20:29 +1000 Subject: [PATCH] =?UTF-8?q?feat(dal):=20UnitFilterService=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitFilterService/UnitFilterService.cs | 84 ++++++++----------- PARR.DAL/ParrDalInstaller.cs | 6 +- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs index be4acf49..ccb61375 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs @@ -7,7 +7,6 @@ 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; @@ -340,12 +339,12 @@ internal class UnitFilterService : IUnitFilterService { if (!relationshipFilters.Any() || !unitIds.Any()) { - logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений", + logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений", unitIds.Count); return unitIds.Select(id => new FilteredUnitContext { UnitId = id }).ToList(); } - logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}", + logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}", unitIds.Count, relationshipFilters.Count()); var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id }); @@ -384,7 +383,7 @@ internal class UnitFilterService : IUnitFilterService : resultContexts.Values.ToList(); var result = unitsWithRelationships; - logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: выход {ContextCount} контекстов (из {InitialCount})", + logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})", result.Count, resultContexts.Count); #if DEBUG @@ -393,8 +392,7 @@ internal class UnitFilterService : IUnitFilterService .Concat(result.SelectMany(c => c.ValidParentIds)) .Concat(result.SelectMany(c => c.ValidChildIds)) .Distinct() - .Take(100) - .ToList(); + .ToList(); // ← Убрали .Take(100) // Пакетная загрузка всех юнитов var unitsMap = await unitService.Get().AsNoTracking() @@ -406,17 +404,18 @@ internal class UnitFilterService : IUnitFilterService 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); + // Логируем только первые 10 юнитов (чтобы не засорять лог) foreach (var context in result.Take(10)) { var u = unitsMap.GetValueOrDefault(context.UnitId); @@ -431,17 +430,16 @@ internal class UnitFilterService : IUnitFilterService { logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); - foreach (var parentId in context.ValidParentIds) + // Логируем только первые 5 родителей (чтобы не засорять лог) + foreach (var parentId in context.ValidParentIds.Take(5)) { var p = unitsMap.GetValueOrDefault(parentId); - logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name, parentId); + logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name ?? "null", parentId); - // Берём значения из кэша, а не делаем запрос if (parentFieldIds.Any() && valuesByUnit.TryGetValue(parentId, out var parentValues)) { foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId))) { - // Используем предварительно загруженный словарь полей var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}"; logger.LogDebug("\t\t {FieldName} = {FieldValue}", fieldName, @@ -449,6 +447,11 @@ internal class UnitFilterService : IUnitFilterService } } } + + if (context.ValidParentIds.Count > 5) + { + logger.LogDebug("\t\t... и ещё {Count} родителей", context.ValidParentIds.Count - 5); + } } else if (parentRelFilters.Any()) { @@ -460,17 +463,15 @@ internal class UnitFilterService : IUnitFilterService { logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); - foreach (var childId in context.ValidChildIds) + foreach (var childId in context.ValidChildIds.Take(5)) { var c = unitsMap.GetValueOrDefault(childId); - logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name, childId); + logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name ?? "null", childId); - // Берём значения из кэша, а не делаем запрос if (childFieldIds.Any() && valuesByUnit.TryGetValue(childId, out var childValues)) { foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId))) { - // Используем предварительно загруженный словарь полей var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}"; logger.LogDebug("\t\t {FieldName} = {FieldValue}", fieldName, @@ -478,6 +479,11 @@ internal class UnitFilterService : IUnitFilterService } } } + + if (context.ValidChildIds.Count > 5) + { + logger.LogDebug("\t\t... и ещё {Count} детей", context.ValidChildIds.Count - 5); + } } else if (childRelFilters.Any()) { @@ -761,7 +767,6 @@ internal class UnitFilterService : IUnitFilterService var directionName = isParentDirection ? "Родительские" : "Дочерние"; logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count); - // Получаем все связи в нужном направлении var allLinks = await unitInUnitService.Get() .AsNoTracking() .Where(link => isParentDirection @@ -780,11 +785,9 @@ internal class UnitFilterService : IUnitFilterService isParentDirection ? "родителей" : "детей", allLinks.Count); - // Группируем связи по исходному юниту для быстрого доступа var linksBySource = allLinks.GroupBy(l => l.SourceId) .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); - // Находим связи, которые проходят ВСЕ фильтры одновременно var targetFilterPassCount = new Dictionary(); foreach (var relFilter in relFilters) @@ -793,7 +796,6 @@ internal class UnitFilterService : IUnitFilterService if (string.IsNullOrWhiteSpace(valueMask)) continue; - // Обработка маски LIKE bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); string dbValueMask; @@ -811,27 +813,27 @@ internal class UnitFilterService : IUnitFilterService isParentDirection ? "Parent" : "Child", fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); - // Находим целевые юниты, которые соответствуют ТЕКУЩЕМУ фильтру - var matchingTargetIds = await unitService.Get() + var matchingTargetIds = await unitInValueService.Get() .AsNoTracking() - .Where(u => allTargetIds.Contains(u.Id)) - .Where(u => u.UnitValues.Any(v => - v.FieldId == relFilter.FieldId && - EF.Functions.ILike(v.Value.Value, dbValueMask))) - .Select(u => u.Id) + .Where(uiv => uiv.FieldId == relFilter.FieldId) + .Where(uiv => EF.Functions.ILike(uiv.Value.Value, dbValueMask)) + .Select(uiv => uiv.UnitId) + .Distinct() .ToListAsync(cancellationToken); + var filteredMatchingTargetIds = matchingTargetIds + .Intersect(allTargetIds) + .ToList(); + logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске", - matchingTargetIds.Count, + filteredMatchingTargetIds.Count, isParentDirection ? "родителей" : "детей"); - // Считаем, сколько фильтров прошёл каждый TargetId if (relFilter.IsInverse) { - // Для IsInverse: "проходит" фильтр тот, кто НЕ соответствует маске foreach (var targetId in allTargetIds) { - if (!matchingTargetIds.Contains(targetId)) + if (!filteredMatchingTargetIds.Contains(targetId)) { if (!targetFilterPassCount.ContainsKey(targetId)) targetFilterPassCount[targetId] = 0; @@ -840,13 +842,12 @@ internal class UnitFilterService : IUnitFilterService } logger.LogDebug(" Для IsInverse: {NonMatchCount} {TargetType} НЕ соответствуют маске", - allTargetIds.Count - matchingTargetIds.Count, + allTargetIds.Count - filteredMatchingTargetIds.Count, isParentDirection ? "родителей" : "детей"); } else { - // Для обычного фильтра: "проходит" тот, кто соответствует маске - foreach (var targetId in matchingTargetIds) + foreach (var targetId in filteredMatchingTargetIds) { if (!targetFilterPassCount.ContainsKey(targetId)) targetFilterPassCount[targetId] = 0; @@ -855,7 +856,6 @@ internal class UnitFilterService : IUnitFilterService } } - // Оставляем только те связи, которые прошли ВСЕ фильтры int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim())); var validTargetIds = targetFilterPassCount .Where(kvp => kvp.Value == requiredFiltersCount) @@ -867,10 +867,8 @@ internal class UnitFilterService : IUnitFilterService isParentDirection ? "родителей" : "детей", requiredFiltersCount); - // Список юнитов, которые не прошли фильтры (для удаления) var unitsToRemove = new HashSet(); - // Применяем к каждому юниту foreach (var context in resultContexts.Values) { if (unitsToRemove.Contains(context.UnitId)) @@ -879,11 +877,9 @@ internal class UnitFilterService : IUnitFilterService var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List()); bool hasTargets = unitTargets.Any(); - // Находим валидные связи (прошли ВСЕ фильтры) var validTargets = unitTargets.Intersect(validTargetIds).ToList(); bool hasValidTargets = validTargets.Any(); - // Логика с IsFullMatch bool isFullMatch = relFilters.All(rf => rf.IsFullMatch); bool isInverse = relFilters.Any(rf => rf.IsInverse); @@ -891,26 +887,21 @@ internal class UnitFilterService : IUnitFilterService if (!hasTargets) { - // Нет связей вообще - passesFilter = isInverse; // IsInverse=true → проходит, иначе нет + passesFilter = isInverse; } else if (isFullMatch) { - // ВСЕ связи должны быть валидными passesFilter = validTargets.Count == unitTargets.Count; } else { - // ХОТЯ БЫ ОДНА связь должна быть валидной passesFilter = hasValidTargets; } if (passesFilter) { - // Сохраняем связи if (isFullMatch) { - // Для IsFullMatch=true: сохраняем ВСЕ связи (они все валидные) if (isParentDirection) context.ValidParentIds.UnionWith(unitTargets); else @@ -918,7 +909,6 @@ internal class UnitFilterService : IUnitFilterService } else { - // Для IsFullMatch=false: сохраняем ТОЛЬКО валидные связи if (isParentDirection) context.ValidParentIds.UnionWith(validTargets); else @@ -927,12 +917,10 @@ internal class UnitFilterService : IUnitFilterService } else { - // Юнит не прошёл фильтр — помечаем на удаление unitsToRemove.Add(context.UnitId); } } - // Удаляем юниты, которые не прошли фильтры foreach (var unitId in unitsToRemove) { resultContexts.Remove(unitId); diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 5a92befb..1b073577 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -151,7 +151,10 @@ namespace PARR.DAL services.AddTransient(); services.AddTransient(); - services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 50 }); + services.Configure(options => + { + options.LoadBatchSize = 50; + }); #endregion @@ -195,6 +198,5 @@ namespace PARR.DAL return new ScheduleResponseAreaTimeOffsetService(dbContext, settingsFromDb, logger); }); } - } }