feat(dal): UnitFilterService оптимизация запросов.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user