using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces; using PARR.Core.Repositories.Interfaces.Unit; using PARR.Domain.Cache; using PARR.Domain.DTOs.UnitDto; namespace PARR.Core.Services.UnitService.Implementations { /// /// Сервис по работе с юнитами из кэш и бд /// internal class UnitCacheService { /// /// TTL для кэш /// private readonly TimeSpan CacheTtl = TimeSpan.FromHours(24); private readonly ILogger logger; private readonly IUnitRepository unitRepository; private readonly IRedisCacheService redisCacheService; public UnitCacheService( ILogger logger, IUnitRepository unitRepository, IRedisCacheService redisCacheService ) { this.logger = logger; this.unitRepository = unitRepository; this.redisCacheService = redisCacheService; } /// /// Получить UnitInfo по id /// /// /// public async Task GetAsync(Guid id) { var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(id)); var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(id)); // Смотрим, есть ли в кэш var cache = await redisCacheService.GetHashFieldAsync(hashKey, unitKey, true); if (cache != null) { logger.LogDebug("Данные о юните {UnitId} получены из кэша.", id); return cache; } // Нет в кэш, ищем в бд var dbData = await GetUnitAsync(id); if (dbData == null) { logger.LogWarning("В БД нет данных о юните {UnitId}. Вернулся null", id); return null; } // Сохраняем в кэш await redisCacheService.SetHashFieldAsync(hashKey, unitKey, dbData, CacheTtl, true); logger.LogDebug("Вернули юнит полученный из БД {UnitId}.", id); return dbData; } /// /// Получить словарь UnitInfo /// /// /// public async Task> GetAsync(IReadOnlySet ids) { if (ids == null || ids.Count == 0) return new Dictionary(); // Формируем ключи var keys = ids.Select(t => { var (hashKey, fieldKey) = (redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(t)), redisCacheService.GetKey(CacheKeys.Unit.UnitItem(t))); return (HashKey: hashKey, FieldKey: fieldKey); }).ToList(); logger.LogDebug("Необходимо найти инорфмацию для {Count} объектов.", ids.Count); // Получаем из кэш var cacheData = await redisCacheService.GetHashFieldsAsync(keys, true); logger.LogDebug("Из кэша получена информация о {Count} объектах.", cacheData.Count); var resultDictionary = cacheData.ToDictionary(unit => unit.Id, unit => unit); // Проверям каких данных нет в кэш, догрузим из из БД if (resultDictionary.Count < ids.Count) { // Находим id которых нет в кэше var missingIds = ids.Where(id => !resultDictionary.ContainsKey(id)).ToHashSet(); logger.LogDebug("Запрашиваем из БД дополнительно {Count} юнитов", missingIds.Count); var dbData = await GetUnitsAsync(missingIds); foreach (var unit in dbData) { resultDictionary[unit.Id] = unit; // Добавляем в кэш var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(unit.Id)); var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(unit.Id)); await redisCacheService.SetHashFieldAsync(hashKey, unitKey, unit, CacheTtl, true); } } // сохранить докаченные в кэш + ттл + не забыть указать сжатие (может вообще этот метод вынести отдельно, так как пересекается когда по одному) return resultDictionary; } /// /// Удалить запись UnitInfo из кэш /// /// /// /// public Task RemoveFromCacheAsync(Guid id) { //todo: может удалять тоже списком throw new NotImplementedException(); } /// /// Получить значение по одному юниту из БД /// /// /// private async Task GetUnitAsync(Guid unitId) { var unit = await unitRepository.Get() .AsNoTracking() .Select(t => new { Id = t.Id, Name = t.Name, Values = t.UnitValues.Select(x => new { FieldId = x.FieldId, Value = x.Value!.Value }), ParentUnits = t.ParentUnits.Select(t => t.ParentUnitId), ChildUnits = t.ChildUnits.Select(t => t.ChildUnitId) }) .FirstOrDefaultAsync(t => t.Id == unitId); if (unit == null) return null; // Склеим вместе родителей и детей, чтоб не делать лишних запросов к бд var relatives = unit.ParentUnits.Union(unit.ChildUnits).ToList(); // Получаем базовую инфу о родственниках var relativesDictionary = await unitRepository.Get() .AsNoTracking() .Where(t => relatives.Contains(t.Id)) .Select(t => new UnitInfoBase { Id = t.Id, Name = t.Name, Values = t.UnitValues.Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value!.Value }).ToList() }).ToDictionaryAsync(t => t.Id); // Собираем итоговый UnitInfo return new UnitInfo { Id = unit.Id, Name = unit.Name, Values = unit.Values.Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value }).ToList(), ParentUnits = unit.ParentUnits .Where(id => relativesDictionary.ContainsKey(id)) .Select(id => relativesDictionary[id]) .ToList(), ChildUnits = unit.ChildUnits .Where(id => relativesDictionary.ContainsKey(id)) .Select(id => relativesDictionary[id]) .ToList() }; } /// /// Получить значения по списку юнитов /// /// /// private async Task> GetUnitsAsync(IReadOnlySet ids) { if (ids == null || ids.Count == 0) return new List(); // Пакетно получаем основные данные для юнитов var dbUnits = new List(); // Выполняем порционно foreach (var chank in ids.Chunk(500)) { var chunkUnits = await unitRepository.Get() .AsNoTracking() .Where(t => chank.Contains(t.Id)) .Select(t => new { Id = t.Id, Name = t.Name, Values = t.UnitValues.Select(x => new { FieldId = x.FieldId, Value = x.Value!.Value }).ToList(), ParentUnits = t.ParentUnits.Select(p => p.ParentUnitId).ToList(), ChildUnits = t.ChildUnits.Select(c => c.ChildUnitId).ToList() }) .ToListAsync(); dbUnits.AddRange(chunkUnits); } if (dbUnits.Count == 0) return new List(); // Собираем ВСЕ ID родственников(и родителей, и детей) для ВСЕХ найденных юнитов в один HashSet var allRelativeIds = new HashSet(); foreach (var unit in dbUnits) { foreach (var parentId in unit.ParentUnits) allRelativeIds.Add(parentId); foreach (var childId in unit.ChildUnits) allRelativeIds.Add(childId); } // Пакетно загружаем базовую информацию обо всех родственниках за один раз var relativesDictionary = new Dictionary(); foreach (var chunk in allRelativeIds.Chunk(500)) { var chunkRelatives = await unitRepository.Get() .AsNoTracking() .Where(t => chunk.Contains(t.Id)) .Select(t => new UnitInfoBase { Id = t.Id, Name = t.Name, Values = t.UnitValues.Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value!.Value }).ToList() }) .ToDictionaryAsync(t => t.Id); foreach (var kvp in chunkRelatives) { relativesDictionary[kvp.Key] = kvp.Value; } } // Собираем итоговый список UnitInfo, маппим родственников из словаря var result = new List(dbUnits.Count); foreach (var unit in dbUnits) { var unitInfo = new UnitInfo { Id = unit.Id, Name = unit.Name, Values = ((IEnumerable)unit.Values) .Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value }) .ToList(), ParentUnits = ((IEnumerable)unit.ParentUnits) .Where(id => relativesDictionary.ContainsKey(id)) .Select(id => relativesDictionary[id]) .ToList(), ChildUnits = ((IEnumerable)unit.ChildUnits) .Where(id => relativesDictionary.ContainsKey(id)) .Select(id => relativesDictionary[id]) .ToList() }; result.Add(unitInfo); } logger.LogDebug("Получено из БД данных для {Count} юнитов", result.Count); return result; } } }