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

333 lines
14 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);
/// <summary>
/// Размер порции данных, для запроса в БД
/// </summary>
private readonly int DatabaseBatchSize = 500;
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);
// список объектов для сохранения в кэш
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<UnitInfo>(dataToCache, CacheTtl, true);
}
return resultDictionary;
}
/// <summary>
/// Удалить запись UnitInfo из кэш
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
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);
}
/// <summary>
/// Удалить записи UnitInfo из кэш
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
public async Task RemoveFromCacheAsync(IReadOnlySet<Guid> 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);
}
/// <summary>
/// Получить значение по одному юниту из БД
/// </summary>
/// <param name="unitId"></param>
/// <returns></returns>
private async Task<UnitInfo?> 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()
};
}
/// <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(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<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(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<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;
}
}
}