Files
parr_api/PARR.Core/Services/UnitService/Implementations/UnitCacheService.cs

298 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// Сервис по работе с юнитами из кэш и бд
/// </summary>
internal class UnitCacheService
{
/// <summary>
/// TTL для кэш
/// </summary>
private readonly TimeSpan CacheTtl = TimeSpan.FromHours(24);
private readonly ILogger<UnitCacheService> logger;
private readonly IUnitRepository unitRepository;
private readonly IRedisCacheService redisCacheService;
public UnitCacheService(
ILogger<UnitCacheService> logger,
IUnitRepository unitRepository,
IRedisCacheService redisCacheService
)
{
this.logger = logger;
this.unitRepository = unitRepository;
this.redisCacheService = redisCacheService;
}
/// <summary>
/// Получить UnitInfo по id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<UnitInfo?> GetAsync(Guid id)
{
var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(id));
var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(id));
// Смотрим, есть ли в кэш
var cache = await redisCacheService.GetHashFieldAsync<UnitInfo?>(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;
}
/// <summary>
/// Получить словарь UnitInfo
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
public async Task<Dictionary<Guid, UnitInfo>> GetAsync(IReadOnlySet<Guid> ids)
{
if (ids == null || ids.Count == 0)
return new Dictionary<Guid, UnitInfo>();
// Формируем ключи
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<UnitInfo>(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;
}
/// <summary>
/// Удалить запись UnitInfo из кэш
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task RemoveFromCacheAsync(Guid id)
{
//todo: может удалять тоже списком
throw new NotImplementedException();
}
/// <summary>
/// Получить значение по одному юниту из БД
/// </summary>
/// <param name="unitId"></param>
/// <returns></returns>
private async Task<UnitInfo?> 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()
};
}
/// <summary>
/// Получить значения по списку юнитов
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
private async Task<List<UnitInfo>> GetUnitsAsync(IReadOnlySet<Guid> ids)
{
if (ids == null || ids.Count == 0)
return new List<UnitInfo>();
// Пакетно получаем основные данные для юнитов
var dbUnits = new List<dynamic>();
// Выполняем порционно
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<UnitInfo>();
// Собираем ВСЕ ID родственников(и родителей, и детей) для ВСЕХ найденных юнитов в один HashSet
var allRelativeIds = new HashSet<Guid>();
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<Guid, UnitInfoBase>();
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<UnitInfo>(dbUnits.Count);
foreach (var unit in dbUnits)
{
var unitInfo = new UnitInfo
{
Id = unit.Id,
Name = unit.Name,
Values = ((IEnumerable<dynamic>)unit.Values)
.Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value })
.ToList(),
ParentUnits = ((IEnumerable<Guid>)unit.ParentUnits)
.Where(id => relativesDictionary.ContainsKey(id))
.Select(id => relativesDictionary[id])
.ToList(),
ChildUnits = ((IEnumerable<Guid>)unit.ChildUnits)
.Where(id => relativesDictionary.ContainsKey(id))
.Select(id => relativesDictionary[id])
.ToList()
};
result.Add(unitInfo);
}
logger.LogDebug("Получено из БД данных для {Count} юнитов", result.Count);
return result;
}
}
}