feat(core): UnitService - работа с кэш
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PARR.API.Contracts.V1;
|
using PARR.API.Contracts.V1;
|
||||||
using PARR.API.Contracts.V1.Responses.Base;
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
using PARR.API.Controllers.V1.Base;
|
using PARR.API.Controllers.V1.Base;
|
||||||
@@ -7,7 +8,9 @@ using PARR.API.Services.Interfaces;
|
|||||||
using PARR.API.Settings;
|
using PARR.API.Settings;
|
||||||
using PARR.Core.Common.Interfaces;
|
using PARR.Core.Common.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
using PARR.Core.Services.Workload.Implementations;
|
using PARR.Core.Services.Workload.Implementations;
|
||||||
using PARR.Domain.Cache;
|
using PARR.Domain.Cache;
|
||||||
|
|
||||||
@@ -22,6 +25,8 @@ namespace PARR.API.Controllers.V1
|
|||||||
private readonly ITemplateRepository templateService;
|
private readonly ITemplateRepository templateService;
|
||||||
private readonly WorkloadCacheService workloadCacheService;
|
private readonly WorkloadCacheService workloadCacheService;
|
||||||
private readonly ILogger<TestController> logger;
|
private readonly ILogger<TestController> logger;
|
||||||
|
private readonly IUnitService unitService;
|
||||||
|
private readonly IUnitRepository unitRepository;
|
||||||
|
|
||||||
public TestController(
|
public TestController(
|
||||||
IClientService clientService,
|
IClientService clientService,
|
||||||
@@ -30,7 +35,9 @@ namespace PARR.API.Controllers.V1
|
|||||||
ITemplateRepository templateService,
|
ITemplateRepository templateService,
|
||||||
MqSettings mqSettings,
|
MqSettings mqSettings,
|
||||||
WorkloadCacheService workloadCacheService,
|
WorkloadCacheService workloadCacheService,
|
||||||
ILogger<TestController> logger
|
ILogger<TestController> logger,
|
||||||
|
IUnitService unitService,
|
||||||
|
IUnitRepository unitRepository
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.clientService = clientService;
|
this.clientService = clientService;
|
||||||
@@ -39,6 +46,8 @@ namespace PARR.API.Controllers.V1
|
|||||||
this.templateService = templateService;
|
this.templateService = templateService;
|
||||||
this.workloadCacheService = workloadCacheService;
|
this.workloadCacheService = workloadCacheService;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
this.unitService = unitService;
|
||||||
|
this.unitRepository = unitRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -80,24 +89,38 @@ namespace PARR.API.Controllers.V1
|
|||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost(ApiRoutes.Test.CreateCache)]
|
[HttpPost(ApiRoutes.Test.CreateCache)]
|
||||||
public async Task<IActionResult> CreateCache()
|
public async Task<IActionResult> CreateCache([FromBody] Guid unitId)
|
||||||
{
|
{
|
||||||
#region Test bucket id
|
var units = await unitRepository.Get().Take(100).Select(t => t.Id).ToListAsync();
|
||||||
|
var listData = await unitService.GetWithCachingAsync(units);
|
||||||
|
//foreach(var unit in units)
|
||||||
|
//{
|
||||||
|
// //поштучно
|
||||||
|
// var unitTtt = await unitService.GetWithCachingAsync(unit);
|
||||||
|
//}
|
||||||
|
|
||||||
var idList = new List<Guid>();
|
return Ok();
|
||||||
|
|
||||||
for (int i = 0; i < 100; i++)
|
//var unit = await unitService.GetWithCachingAsync(unitId);
|
||||||
idList.Add( Guid.NewGuid());
|
|
||||||
|
|
||||||
foreach(var id in idList.OrderBy(t => t))
|
|
||||||
{
|
|
||||||
var key = CacheKeys.Unit.UnitHashWithBucket(id);
|
|
||||||
var redisKey = redisCacheService.GetKey(key);
|
|
||||||
|
|
||||||
logger.LogDebug("Id: {Id}, key: '{Key}', redis key: '{RedisKey}'", id, key, redisKey);
|
//return Ok(unit);
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
//#region Test bucket id
|
||||||
|
|
||||||
|
//var idList = new List<Guid>();
|
||||||
|
|
||||||
|
//for (int i = 0; i < 100; i++)
|
||||||
|
// idList.Add( Guid.NewGuid());
|
||||||
|
|
||||||
|
//foreach(var id in idList.OrderBy(t => t))
|
||||||
|
//{
|
||||||
|
// var key = CacheKeys.Unit.UnitHashWithBucket(id);
|
||||||
|
// var redisKey = redisCacheService.GetKey(key);
|
||||||
|
|
||||||
|
// logger.LogDebug("Id: {Id}, key: '{Key}', redis key: '{RedisKey}'", id, key, redisKey);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
// var aaa = await workloadCacheService.GetTemplateReportDataAsync();
|
// var aaa = await workloadCacheService.GetTemplateReportDataAsync();
|
||||||
|
|||||||
@@ -82,8 +82,9 @@
|
|||||||
/// <param name="field"></param>
|
/// <param name="field"></param>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
/// <param name="ttl">Если указано, обновится у всего Hash. Если не указано и hash не существовал, создастся Hash с бесокнечным ttl</param>
|
/// <param name="ttl">Если указано, обновится у всего Hash. Если не указано и hash не существовал, создастся Hash с бесокнечным ttl</param>
|
||||||
|
/// <param name="useCompression">Сжимать данные</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task SetHashFieldAsync<T>(string hashKey, string field, T value, TimeSpan? ttl = null);
|
Task SetHashFieldAsync<T>(string hashKey, string field, T value, TimeSpan? ttl = null, bool useCompression = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получить значение поля из Hash
|
/// Получить значение поля из Hash
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
/// <param name="hashKey"></param>
|
/// <param name="hashKey"></param>
|
||||||
/// <param name="field"></param>
|
/// <param name="field"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<T?> GetHashFieldAsync<T>(string hashKey, string field);
|
Task<T?> GetHashFieldAsync<T>(string hashKey, string field, bool useCompression = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получить случайную первую запись из Hash
|
/// Получить случайную первую запись из Hash
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T"></typeparam>
|
||||||
/// <param name="hashKey"></param>
|
/// <param name="hashKey"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<(string Field, T? Value)?> GetFirstHashFieldAsync<T>(string hashKey);
|
Task<(string Field, T? Value)?> GetFirstHashFieldAsync<T>(string hashKey, bool useCompression = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получить все значения из Hash
|
/// Получить все значения из Hash
|
||||||
@@ -108,7 +109,7 @@
|
|||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T"></typeparam>
|
||||||
/// <param name="hashKey"></param>
|
/// <param name="hashKey"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<Dictionary<string, T>> GetAllHashFieldsAsync<T>(string hashKey);
|
Task<Dictionary<string, T>> GetAllHashFieldsAsync<T>(string hashKey, bool useCompression = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Удалить одну запись из Hash
|
/// Удалить одну запись из Hash
|
||||||
@@ -148,6 +149,15 @@
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task SetHashTtlAsync(string hashKey, TimeSpan ttl);
|
Task SetHashTtlAsync(string hashKey, TimeSpan ttl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить значения нескольких полей из Hash
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="keys"></param>
|
||||||
|
/// <param name="useCompression"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<T>> GetHashFieldsAsync<T>(List<(string HashKey, string FieldKey)> keys, bool useCompression = false);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ using PARR.Core.Services.TaskServices.Providers;
|
|||||||
using PARR.Core.Services.TaskServices.ReconciliationHosted;
|
using PARR.Core.Services.TaskServices.ReconciliationHosted;
|
||||||
using PARR.Core.Services.UnitFilterService;
|
using PARR.Core.Services.UnitFilterService;
|
||||||
using PARR.Core.Services.UnitFilterService.Models;
|
using PARR.Core.Services.UnitFilterService.Models;
|
||||||
|
using PARR.Core.Services.UnitService.Implementations;
|
||||||
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
using PARR.Core.Services.Workload.Implementations;
|
using PARR.Core.Services.Workload.Implementations;
|
||||||
using PARR.Core.Services.Workload.Interfaces;
|
using PARR.Core.Services.Workload.Interfaces;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
@@ -103,6 +105,9 @@ namespace PARR.Core
|
|||||||
|
|
||||||
services.AddScoped<IRobotTaskService, RobotTaskService>();
|
services.AddScoped<IRobotTaskService, RobotTaskService>();
|
||||||
|
|
||||||
|
services.AddScoped<IUnitService, UnitService>();
|
||||||
|
services.AddScoped<UnitCacheService>();
|
||||||
|
|
||||||
//services.AddScoped<IUserService, UserService>();
|
//services.AddScoped<IUserService, UserService>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
// Получаем из кэш
|
||||||
|
var cacheData = await redisCacheService.GetHashFieldsAsync<UnitInfo>(keys, true);
|
||||||
|
|
||||||
|
// проверить, если получили все элементы, вернуть
|
||||||
|
// если не все, докачать из бд
|
||||||
|
// сохранить докаченные в кэш + ттл + не забыть указать сжатие (может вообще этот метод вынести отдельно, так как пересекается когда по одному)
|
||||||
|
|
||||||
|
//var dbData = await GetUnitsAsync(ids);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//todo:
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
|
using PARR.Domain.DTOs.UnitDto;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.UnitService.Implementations
|
||||||
|
{
|
||||||
|
internal class UnitService : IUnitService
|
||||||
|
{
|
||||||
|
private readonly ILogger<UnitService> logger;
|
||||||
|
private readonly UnitCacheService unitCacheService;
|
||||||
|
|
||||||
|
public UnitService(
|
||||||
|
ILogger<UnitService> logger,
|
||||||
|
UnitCacheService unitCacheService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.unitCacheService = unitCacheService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<UnitInfo?> GetWithCachingAsync(Guid id)
|
||||||
|
{
|
||||||
|
return await unitCacheService.GetAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Guid, UnitInfo>> GetWithCachingAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
// Защищаем себя от Multiple Enumeration и убираем дубликаты, если передали List
|
||||||
|
var uniqueIds = ids as IReadOnlySet<Guid> ?? ids.ToHashSet();
|
||||||
|
|
||||||
|
return await unitCacheService.GetAsync(uniqueIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveFromCacheAsync(Guid id)
|
||||||
|
{
|
||||||
|
await unitCacheService.RemoveFromCacheAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
PARR.Core/Services/UnitService/Interfaces/IUnitService.cs
Normal file
35
PARR.Core/Services/UnitService/Interfaces/IUnitService.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using PARR.Domain.DTOs.UnitDto;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.UnitService.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис управления юнитами
|
||||||
|
/// </summary>
|
||||||
|
public interface IUnitService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получить юнит со всем значениями.
|
||||||
|
/// Используется кэш.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<UnitInfo?> GetWithCachingAsync(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить словарь юнитов со всеми значениями.
|
||||||
|
/// Используется кэш.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ids"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<Dictionary<Guid, UnitInfo>> GetWithCachingAsync(IEnumerable<Guid> ids);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удалить юнит из кэш.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task RemoveFromCacheAsync(Guid id);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
52
PARR.Domain/DTOs/UnitDto/UnitInfo.cs
Normal file
52
PARR.Domain/DTOs/UnitDto/UnitInfo.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
namespace PARR.Domain.DTOs.UnitDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Юнит с ЕСПП атрибутами, детьми и родителями и их атрибутами
|
||||||
|
/// </summary>
|
||||||
|
public record UnitInfo : UnitInfoBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Родительские юниты
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<UnitInfoBase> ParentUnits { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дочерние юниты
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<UnitInfoBase> ChildUnits { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UnitInfoBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id юнита
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ЭК
|
||||||
|
/// </summary>
|
||||||
|
public required string Name { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Атрибуты из ЕСПП
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<UnitInfoAttribute> Values { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Атрибут ЕСПП
|
||||||
|
/// </summary>
|
||||||
|
public record UnitInfoAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ИД поля
|
||||||
|
/// </summary>
|
||||||
|
public Guid FieldId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Значение
|
||||||
|
/// </summary>
|
||||||
|
public string? Value { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using PARR.Core.Common.Interfaces;
|
using PARR.Core.Common.Interfaces;
|
||||||
using PARR.Infrastructure.Redis.Helpers;
|
using PARR.Infrastructure.Redis.Helpers;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
@@ -48,7 +49,7 @@ namespace PARR.Infrastructure.Redis
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception("Формат данных не байты.", ex);
|
throw new InvalidOperationException("Формат данных не байты.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -65,7 +66,7 @@ namespace PARR.Infrastructure.Redis
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception("Формат данных не строка.", ex);
|
throw new InvalidOperationException("Формат данных не строка.", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -105,8 +106,8 @@ namespace PARR.Infrastructure.Redis
|
|||||||
if (useCompression)
|
if (useCompression)
|
||||||
{
|
{
|
||||||
// храним в байтах, если надо то сжимаем
|
// храним в байтах, если надо то сжимаем
|
||||||
var jsonData = SerializeWithCompression(data);
|
var byteData = SerializeWithCompression(data);
|
||||||
await cache.SetAsync(key, jsonData, options);
|
await cache.SetAsync(key, byteData, options);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -187,14 +188,24 @@ namespace PARR.Infrastructure.Redis
|
|||||||
|
|
||||||
#region Нативные операции Redis, Redis Hash
|
#region Нативные операции Redis, Redis Hash
|
||||||
|
|
||||||
public async Task SetHashFieldAsync<T>(string hashKey, string field, T value, TimeSpan? ttl = null)
|
public async Task SetHashFieldAsync<T>(string hashKey, string field, T value, TimeSpan? ttl = null, bool useCompression = false)
|
||||||
{
|
{
|
||||||
// меняет одно поле в Hash
|
// меняет одно поле в Hash
|
||||||
|
|
||||||
var jsonData = JsonSerializer.Serialize(value);
|
if (useCompression)
|
||||||
|
{
|
||||||
// true - поля не было, создалось новое. false - поле было, обновили значение
|
// храним в байтах, если надо то сжимаем
|
||||||
var result = await redis.HashSetAsync(hashKey, field, jsonData);
|
var byteData = SerializeWithCompression(value);
|
||||||
|
// true - поля не было, создалось новое. false - поле было, обновили значение
|
||||||
|
var result = await redis.HashSetAsync(hashKey, field, byteData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// храним в строке
|
||||||
|
var jsonData = JsonSerializer.Serialize(value);
|
||||||
|
// true - поля не было, создалось новое. false - поле было, обновили значение
|
||||||
|
var result = await redis.HashSetAsync(hashKey, field, jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
// если указан ttl, обновим для всего Hash
|
// если указан ttl, обновим для всего Hash
|
||||||
// если не указан и ранее был создан hashKey, оставит его ttl; а если hashKey не было, то создаст его БЕССРОЧНЫМ!!!
|
// если не указан и ранее был создан hashKey, оставит его ttl; а если hashKey не было, то создаст его БЕССРОЧНЫМ!!!
|
||||||
@@ -202,7 +213,7 @@ namespace PARR.Infrastructure.Redis
|
|||||||
await SetHashTtlAsync(hashKey, ttl.Value);
|
await SetHashTtlAsync(hashKey, ttl.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T?> GetHashFieldAsync<T>(string hashKey, string field)
|
public async Task<T?> GetHashFieldAsync<T>(string hashKey, string field, bool useCompression = false)
|
||||||
{
|
{
|
||||||
// получить значение поля из Hash
|
// получить значение поля из Hash
|
||||||
var value = await redis.HashGetAsync(hashKey, field);
|
var value = await redis.HashGetAsync(hashKey, field);
|
||||||
@@ -210,10 +221,80 @@ namespace PARR.Infrastructure.Redis
|
|||||||
if (value.IsNullOrEmpty)
|
if (value.IsNullOrEmpty)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(value);
|
if (useCompression)
|
||||||
|
{
|
||||||
|
// хранили в байтах
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DeserializeWithCompression<T>(value);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Формат данных не байты.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// хранили в строке
|
||||||
|
return JsonSerializer.Deserialize<T>(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(string Field, T? Value)?> GetFirstHashFieldAsync<T>(string hashKey)
|
public async Task<List<T>> GetHashFieldsAsync<T>(List<(string HashKey, string FieldKey)> keys, bool useCompression = false)
|
||||||
|
{
|
||||||
|
var allItems = new List<T>(keys.Count);
|
||||||
|
|
||||||
|
// Делим на порции по 5000 (оптимально)
|
||||||
|
foreach (var chunk in keys.Chunk(5000))
|
||||||
|
{
|
||||||
|
// Формируем пайплайн ТОЛЬКО для 5 000 элементов
|
||||||
|
var tasks = new List<Task<RedisValue>>(chunk.Length);
|
||||||
|
foreach (var key in chunk)
|
||||||
|
{
|
||||||
|
tasks.Add(redis.HashGetAsync(key.HashKey, key.FieldKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Сформировал {Count} одновременных запросов в кэш", tasks.Count);
|
||||||
|
|
||||||
|
// Ждем ответа от текущей порции
|
||||||
|
RedisValue[] results = await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
// Парсим
|
||||||
|
foreach (var result in results)
|
||||||
|
{
|
||||||
|
if (result.HasValue)
|
||||||
|
{
|
||||||
|
if (useCompression)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var item = DeserializeWithCompression<T>((byte[])result);
|
||||||
|
if (item != null)
|
||||||
|
allItems.Add(item);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Формат данных не байты.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var item = JsonSerializer.Deserialize<T>(result.ToString());
|
||||||
|
if (item != null)
|
||||||
|
allItems.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Получил {Count} элементов из кэш.", allItems.Count);
|
||||||
|
|
||||||
|
return allItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string Field, T? Value)?> GetFirstHashFieldAsync<T>(string hashKey, bool useCompression = false)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Запрос первого поля из хеша '{HashKey}'", hashKey);
|
logger.LogDebug("Запрос первого поля из хеша '{HashKey}'", hashKey);
|
||||||
|
|
||||||
@@ -234,7 +315,24 @@ namespace PARR.Infrastructure.Redis
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deserialized = JsonSerializer.Deserialize<T>(entry.Value);
|
T? deserialized = default;
|
||||||
|
|
||||||
|
if (useCompression)
|
||||||
|
{
|
||||||
|
// хранили в байтах
|
||||||
|
try
|
||||||
|
{
|
||||||
|
deserialized = DeserializeWithCompression<T>(entry.Value);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Формат данных не байты.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deserialized = JsonSerializer.Deserialize<T>(entry.Value);
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogDebug("Получено поле '{Field}' из хеша '{HashKey}', тип: {Type}", entry.Name, hashKey, typeof(T).Name);
|
logger.LogDebug("Получено поле '{Field}' из хеша '{HashKey}', тип: {Type}", entry.Name, hashKey, typeof(T).Name);
|
||||||
|
|
||||||
@@ -245,7 +343,7 @@ namespace PARR.Infrastructure.Redis
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<string, T>> GetAllHashFieldsAsync<T>(string hashKey)
|
public async Task<Dictionary<string, T>> GetAllHashFieldsAsync<T>(string hashKey, bool useCompression = false)
|
||||||
{
|
{
|
||||||
// получить все записи из Hash
|
// получить все записи из Hash
|
||||||
var objs = await redis.HashGetAllAsync(hashKey);
|
var objs = await redis.HashGetAllAsync(hashKey);
|
||||||
@@ -253,12 +351,20 @@ namespace PARR.Infrastructure.Redis
|
|||||||
if (objs.Length == 0)
|
if (objs.Length == 0)
|
||||||
return new Dictionary<string, T>();
|
return new Dictionary<string, T>();
|
||||||
|
|
||||||
var result = objs.ToDictionary(
|
try
|
||||||
|
{
|
||||||
|
var result = objs.ToDictionary(
|
||||||
t => t.Name.ToString(),
|
t => t.Name.ToString(),
|
||||||
t => JsonSerializer.Deserialize<T>(t.Value)
|
t => useCompression ? DeserializeWithCompression<T>(t.Value) : JsonSerializer.Deserialize<T>(t.Value)
|
||||||
);
|
);
|
||||||
|
|
||||||
return result!;
|
return result!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception("Не смог десериализовать полученные данные", ex);
|
||||||
|
//return new Dictionary<string, T>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteHashFieldAsync(string hashKey, string field)
|
public async Task DeleteHashFieldAsync(string hashKey, string field)
|
||||||
|
|||||||
Reference in New Issue
Block a user