fix(dal): UnitFilterServicen исправлена инверсивная фильтрация связанных ЭК
This commit is contained in:
@@ -2,6 +2,6 @@
|
||||
{
|
||||
internal class UnitFilterServiceOptions
|
||||
{
|
||||
public int LoadBatchSize { get; set; } = 100;
|
||||
public int LoadBatchSize { get; set; } = 50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UnitFilterServiceOptions> options
|
||||
IOptions<UnitFilterServiceOptions> 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
|
||||
/// <param name="unitIds"></param>
|
||||
/// <param name="relationshipFilters"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<List<FilteredUnitContext>> ApplyRelationshipFiltersOnDbAsync(
|
||||
private async Task<List<FilteredUnitContext>> ProcessRelationshipFiltersAsync(
|
||||
List<Guid> unitIds,
|
||||
IEnumerable<JobRelationshipFilter> 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 вспомогательные методы
|
||||
/// <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>
|
||||
private async Task ApplyRelationshipFiltersAsync(
|
||||
private async Task ApplyRelationshipFiltersOnDbAsync(
|
||||
List<Guid> unitIds,
|
||||
Dictionary<Guid, FilteredUnitContext> resultContexts,
|
||||
List<JobRelationshipFilter> 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<Guid, int>();
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace PARR.DAL
|
||||
services.AddTransient<IUnitFilterService, UnitFilterService>();
|
||||
services.AddTransient<IMatchingStatusService, MatchingStatusService>();
|
||||
|
||||
services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 1000 });
|
||||
services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 50 });
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user