fix(dal): UnitFilterServicen исправлена инверсивная фильтрация связанных ЭК

This commit is contained in:
Mikhail Kuznetsov
2026-03-16 16:21:16 +10:00
parent 74d7affd8e
commit 8f4ad21a3c
3 changed files with 139 additions and 129 deletions

View File

@@ -2,6 +2,6 @@
{ {
internal class UnitFilterServiceOptions internal class UnitFilterServiceOptions
{ {
public int LoadBatchSize { get; set; } = 100; public int LoadBatchSize { get; set; } = 50;
} }
} }

View File

@@ -7,6 +7,7 @@ using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.UnitFilterService.Models; using PARR.DAL.DomainServices.UnitFilterService.Models;
using PARR.DAL.Models.Job; using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit; using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Implementations.Unit;
using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.Services.Interfaces.Unit;
using System.Diagnostics; using System.Diagnostics;
@@ -26,6 +27,7 @@ internal class UnitFilterService : IUnitFilterService
private readonly IUnitService unitService; private readonly IUnitService unitService;
private readonly IUnitInUnitService unitInUnitService; private readonly IUnitInUnitService unitInUnitService;
private readonly IRedisCacheService cacheService; private readonly IRedisCacheService cacheService;
private readonly IUnitFieldService unitFieldService;
private readonly IUnitInValueService unitInValueService; private readonly IUnitInValueService unitInValueService;
public UnitFilterService( public UnitFilterService(
@@ -35,7 +37,8 @@ internal class UnitFilterService : IUnitFilterService
IUnitInUnitService unitInUnitService, IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService, IUnitInValueService unitInValueService,
IRedisCacheService cacheService, IRedisCacheService cacheService,
IOptions<UnitFilterServiceOptions> options IOptions<UnitFilterServiceOptions> options,
IUnitFieldService unitFieldService
) )
{ {
this.logger = logger; this.logger = logger;
@@ -43,6 +46,7 @@ internal class UnitFilterService : IUnitFilterService
this.unitService = unitService; this.unitService = unitService;
this.unitInUnitService = unitInUnitService; this.unitInUnitService = unitInUnitService;
this.cacheService = cacheService; this.cacheService = cacheService;
this.unitFieldService = unitFieldService;
this.unitInValueService = unitInValueService; this.unitInValueService = unitInValueService;
batchSize = options.Value.LoadBatchSize; batchSize = options.Value.LoadBatchSize;
@@ -138,7 +142,7 @@ internal class UnitFilterService : IUnitFilterService
// 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ FilteredUnitContext // 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ FilteredUnitContext
var relStopwatch = Stopwatch.StartNew(); var relStopwatch = Stopwatch.StartNew();
var relationshipFilteredContexts = await ApplyRelationshipFiltersOnDbAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken); var relationshipFilteredContexts = await ProcessRelationshipFiltersAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
relStopwatch.Stop(); relStopwatch.Stop();
if (!relationshipFilteredContexts.Any()) if (!relationshipFilteredContexts.Any())
@@ -306,22 +310,14 @@ internal class UnitFilterService : IUnitFilterService
{ {
query = query.Where(u => !u.UnitValues.Any(v => query = query.Where(u => !u.UnitValues.Any(v =>
v.FieldId == fieldFilter.FieldId && v.FieldId == fieldFilter.FieldId &&
EF.Functions.Like(v.Value.Value, dbValueMask))); EF.Functions.ILike(v.Value.Value, dbValueMask)));
} }
else else
{ {
query = query.Where(u => u.UnitValues.Any(v => query = query.Where(u => u.UnitValues.Any(v =>
v.FieldId == fieldFilter.FieldId && 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); var result = await query.Select(u => u.Id).ToListAsync(cancellationToken);
@@ -337,7 +333,7 @@ internal class UnitFilterService : IUnitFilterService
/// <param name="unitIds"></param> /// <param name="unitIds"></param>
/// <param name="relationshipFilters"></param> /// <param name="relationshipFilters"></param>
/// <returns></returns> /// <returns></returns>
private async Task<List<FilteredUnitContext>> ApplyRelationshipFiltersOnDbAsync( private async Task<List<FilteredUnitContext>> ProcessRelationshipFiltersAsync(
List<Guid> unitIds, List<Guid> unitIds,
IEnumerable<JobRelationshipFilter> relationshipFilters, IEnumerable<JobRelationshipFilter> relationshipFilters,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -360,9 +356,9 @@ internal class UnitFilterService : IUnitFilterService
// Обработка родительских фильтров // Обработка родительских фильтров
if (parentRelFilters.Any()) if (parentRelFilters.Any())
{ {
await ApplyRelationshipFiltersAsync( await ApplyRelationshipFiltersOnDbAsync(
unitIds, unitIds,
resultContexts, // ← Передаём словарь, а не список resultContexts,
parentRelFilters, parentRelFilters,
isParentDirection: true, isParentDirection: true,
cancellationToken); cancellationToken);
@@ -371,9 +367,9 @@ internal class UnitFilterService : IUnitFilterService
// Обработка дочерних фильтров // Обработка дочерних фильтров
if (childRelFilters.Any()) if (childRelFilters.Any())
{ {
await ApplyRelationshipFiltersAsync( await ApplyRelationshipFiltersOnDbAsync(
unitIds, unitIds,
resultContexts, // ← Передаём словарь, а не список resultContexts,
childRelFilters, childRelFilters,
isParentDirection: false, isParentDirection: false,
cancellationToken); cancellationToken);
@@ -392,85 +388,100 @@ internal class UnitFilterService : IUnitFilterService
result.Count, resultContexts.Count); result.Count, resultContexts.Count);
#if DEBUG #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)) foreach (var context in result.Take(10))
{ {
var u = await unitService.GetAsync(context.UnitId); var u = unitsMap.GetValueOrDefault(context.UnitId);
if (u == null) if (u == null)
continue; continue;
logger.LogDebug("📦 Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id); 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();
// === РОДИТЕЛИ === // === РОДИТЕЛИ ===
if (context.ValidParentIds.Any()) if (context.ValidParentIds.Any())
{ {
logger.LogDebug("\t📌 Фильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count);
foreach (var parentId in context.ValidParentIds) foreach (var parentId in context.ValidParentIds)
{ {
var p = await unitService.GetAsync(parentId); var p = unitsMap.GetValueOrDefault(parentId);
logger.LogDebug("\t\t👤 {ParentName} (Id={ParentId})", p?.Name, 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( foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId)))
new[] { parentId },
parentFieldIds
);
foreach (var pv in parentValues)
{ {
var field = pv.Field; // Используем предварительно загруженный словарь полей
var value = pv.Value?.Value; var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}";
logger.LogDebug("\t\t 🔹 {FieldName} = {FieldValue}", logger.LogDebug("\t\t {FieldName} = {FieldValue}",
field?.AihitName ?? $"FieldId={pv.FieldId}", fieldName,
value ?? "null"); pv.Value?.Value ?? "null");
} }
} }
} }
} }
else if (parentRelFilters.Any()) else if (parentRelFilters.Any())
{ {
logger.LogDebug("\t Родительские фильтры заданы, но подходящих родителей не найдено"); logger.LogDebug("\tРодительские фильтры заданы, но подходящих родителей не найдено");
} }
// === ДЕТИ === // === ДЕТИ ===
if (context.ValidChildIds.Any()) if (context.ValidChildIds.Any())
{ {
logger.LogDebug("\t Фильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count);
foreach (var childId in context.ValidChildIds) foreach (var childId in context.ValidChildIds)
{ {
var c = await unitService.GetAsync(childId); var c = unitsMap.GetValueOrDefault(childId);
logger.LogDebug("\t\t {ChildName} (Id={ChildId})", c?.Name, 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( foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId)))
new[] { childId },
childFieldIds
);
foreach (var cv in childValues)
{ {
var field = cv.Field; // Используем предварительно загруженный словарь полей
var value = cv.Value?.Value; var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}";
logger.LogDebug("\t\t {FieldName} = {FieldValue}", logger.LogDebug("\t\t {FieldName} = {FieldValue}",
field?.AihitName ?? $"FieldId={cv.FieldId}", fieldName,
value ?? "null"); cv.Value?.Value ?? "null");
} }
} }
} }
} }
else if (childRelFilters.Any()) else if (childRelFilters.Any())
{ {
logger.LogDebug("\t Дочерние фильтры заданы, но подходящих детей не найдено"); logger.LogDebug("\tДочерние фильтры заданы, но подходящих детей не найдено");
} }
} }
#endif #endif
@@ -733,57 +744,11 @@ internal class UnitFilterService : IUnitFilterService
} }
#region вспомогательные методы #region вспомогательные методы
/// <summary>
/// Оценивает, проходит ли юнит фильтр связей
/// </summary>
private bool EvaluateFilter(
List<Guid> unitRelations, // Все связи юнита
List<Guid> 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; // ХОТЯ БЫ ОДНА связь соответствует
}
}
/// <summary> /// <summary>
/// Применяет фильтры к связям (родителям или детям) /// Применяет фильтры к связям (родителям или детям)
/// Связь должна пройти ВСЕ фильтры направления /// Связь должна пройти ВСЕ фильтры направления
/// </summary> /// </summary>
private async Task ApplyRelationshipFiltersAsync( private async Task ApplyRelationshipFiltersOnDbAsync(
List<Guid> unitIds, List<Guid> unitIds,
Dictionary<Guid, FilteredUnitContext> resultContexts, Dictionary<Guid, FilteredUnitContext> resultContexts,
List<JobRelationshipFilter> relFilters, List<JobRelationshipFilter> relFilters,
@@ -820,7 +785,6 @@ internal class UnitFilterService : IUnitFilterService
.ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());
// Находим связи, которые проходят ВСЕ фильтры одновременно // Находим связи, которые проходят ВСЕ фильтры одновременно
// Для каждого TargetId считаем, сколько фильтров он прошёл
var targetFilterPassCount = new Dictionary<Guid, int>(); var targetFilterPassCount = new Dictionary<Guid, int>();
foreach (var relFilter in relFilters) foreach (var relFilter in relFilters)
@@ -847,13 +811,13 @@ internal class UnitFilterService : IUnitFilterService
isParentDirection ? "Parent" : "Child", isParentDirection ? "Parent" : "Child",
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
// Находим целевые юниты, которые соответствуют текущему фильтру // Находим целевые юниты, которые соответствуют ТЕКУЩЕМУ фильтру
var matchingTargetIds = await unitService.Get() var matchingTargetIds = await unitService.Get()
.AsNoTracking() .AsNoTracking()
.Where(u => allTargetIds.Contains(u.Id)) .Where(u => allTargetIds.Contains(u.Id))
.Where(u => u.UnitValues.Any(v => .Where(u => u.UnitValues.Any(v =>
v.FieldId == relFilter.FieldId && v.FieldId == relFilter.FieldId &&
EF.Functions.Like(v.Value.Value, dbValueMask))) EF.Functions.ILike(v.Value.Value, dbValueMask)))
.Select(u => u.Id) .Select(u => u.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -862,15 +826,36 @@ internal class UnitFilterService : IUnitFilterService
isParentDirection ? "родителей" : "детей"); isParentDirection ? "родителей" : "детей");
// Считаем, сколько фильтров прошёл каждый TargetId // Считаем, сколько фильтров прошёл каждый TargetId
foreach (var targetId in matchingTargetIds) if (relFilter.IsInverse)
{ {
if (!targetFilterPassCount.ContainsKey(targetId)) // Для IsInverse: "проходит" фильтр тот, кто НЕ соответствует маске
targetFilterPassCount[targetId] = 0; foreach (var targetId in allTargetIds)
targetFilterPassCount[targetId]++; {
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())); int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim()));
var validTargetIds = targetFilterPassCount var validTargetIds = targetFilterPassCount
.Where(kvp => kvp.Value == requiredFiltersCount) .Where(kvp => kvp.Value == requiredFiltersCount)
@@ -898,22 +883,47 @@ internal class UnitFilterService : IUnitFilterService
var validTargets = unitTargets.Intersect(validTargetIds).ToList(); var validTargets = unitTargets.Intersect(validTargetIds).ToList();
bool hasValidTargets = validTargets.Any(); bool hasValidTargets = validTargets.Any();
// Оцениваем, проходит ли юнит фильтр // Логика с IsFullMatch
bool passesFilter = EvaluateFilter( bool isFullMatch = relFilters.All(rf => rf.IsFullMatch);
validTargets, // Только связи, прошедшие ВСЕ фильтры bool isInverse = relFilters.Any(rf => rf.IsInverse);
validTargets, // matchingRelations = validTargets (все валидные)
relFilters.All(rf => rf.IsFullMatch), // IsFullMatch для всех фильтров
relFilters.Any(rf => rf.IsInverse), // IsInverse для всех фильтров
hasTargets
);
if (passesFilter && hasValidTargets) bool passesFilter;
if (!hasTargets)
{ {
// Сохраняем только валидные связи // Нет связей вообще
if (isParentDirection) passesFilter = isInverse; // IsInverse=true → проходит, иначе нет
context.ValidParentIds.UnionWith(validTargets); }
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 else
context.ValidChildIds.UnionWith(validTargets); {
// Для IsFullMatch=false: сохраняем ТОЛЬКО валидные связи
if (isParentDirection)
context.ValidParentIds.UnionWith(validTargets);
else
context.ValidChildIds.UnionWith(validTargets);
}
} }
else else
{ {
@@ -951,7 +961,7 @@ internal class UnitFilterService : IUnitFilterService
} }
var initialUnitIds = await unitService.Get().AsNoTracking() 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) .Select(u => u.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);

View File

@@ -151,7 +151,7 @@ namespace PARR.DAL
services.AddTransient<IUnitFilterService, UnitFilterService>(); services.AddTransient<IUnitFilterService, UnitFilterService>();
services.AddTransient<IMatchingStatusService, MatchingStatusService>(); services.AddTransient<IMatchingStatusService, MatchingStatusService>();
services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 1000 }); services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 50 });
#endregion #endregion