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 int DatabaseBatchSize = 500;
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);
// список объектов для сохранения в кэш
var dataToCache = new List<(string HashKey, string FieldKey, UnitInfo Value)>();
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);
dataToCache.Add((hashKey, unitKey, unit));
}
if (dataToCache.Count > 0)
await redisCacheService.SetHashFieldsAsync(dataToCache, CacheTtl, true);
}
return resultDictionary;
}
///
/// Удалить запись UnitInfo из кэш
///
///
///
///
public async Task RemoveFromCacheAsync(Guid id)
{
var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(id));
var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(id));
await redisCacheService.DeleteHashFieldAsync(hashKey, unitKey);
}
///
/// Удалить записи UnitInfo из кэш
///
///
///
public async Task RemoveFromCacheAsync(IReadOnlySet ids)
{
if (ids == null || ids.Count == 0)
return;
// Формируем ключи
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();
await redisCacheService.DeleteHashFieldsAsync(keys);
}
///
/// Получить значение по одному юниту из БД
///
///
///
private async Task GetUnitAsync(Guid unitId)
{
var unit = await unitRepository.Get()
.AsNoTracking()
.AsSplitQuery()
.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()
.AsSplitQuery()
.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(DatabaseBatchSize))
{
var chunkUnits = await unitRepository.Get()
.AsNoTracking()
.AsSplitQuery()
.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(DatabaseBatchSize))
{
var chunkRelatives = await unitRepository.Get()
.AsNoTracking()
.AsSplitQuery()
.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;
}
}
}