feat(dal): UnitFilterService оптимизация запросов.

This commit is contained in:
Mikhail Kuznetsov
2026-03-17 13:20:29 +10:00
parent 3f013e2bfc
commit 3704a131b4
2 changed files with 40 additions and 50 deletions

View File

@@ -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<Guid, int>();
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<Guid>();
// Применяем к каждому юниту
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<Guid>());
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);

View File

@@ -151,7 +151,10 @@ namespace PARR.DAL
services.AddTransient<IUnitFilterService, UnitFilterService>();
services.AddTransient<IMatchingStatusService, MatchingStatusService>();
services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 50 });
services.Configure<UnitFilterServiceOptions>(options =>
{
options.LoadBatchSize = 50;
});
#endregion
@@ -195,6 +198,5 @@ namespace PARR.DAL
return new ScheduleResponseAreaTimeOffsetService(dbContext, settingsFromDb, logger);
});
}
}
}