102 lines
4.5 KiB
C#
102 lines
4.5 KiB
C#
using ExperionCrawler.Core.Application.Interfaces;
|
|
using ExperionCrawler.Core.Domain.Entities;
|
|
using ExperionCrawler.Infrastructure.Database;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Npgsql;
|
|
|
|
namespace ExperionCrawler.Infrastructure.OpcUa;
|
|
|
|
/// <summary>
|
|
/// 메타데이터(desc, area, state0~7descriptor)를 OPC UA에서 읽어서 tag_metadata 테이블에 저장/갱신
|
|
/// </summary>
|
|
public class MetadataLoaderService : IMetadataLoaderService
|
|
{
|
|
private readonly IExperionOpcClient _opcClient;
|
|
private readonly ExperionDbContext _ctx;
|
|
private readonly ILogger<MetadataLoaderService> _logger;
|
|
|
|
// 로드할 메타데이터 속성 목록
|
|
private static readonly string[] MetaAttributes =
|
|
{
|
|
"desc", "area",
|
|
"state0descriptor", "state1descriptor", "state2descriptor",
|
|
"state3descriptor", "state4descriptor", "state5descriptor",
|
|
"state6descriptor", "state7descriptor"
|
|
};
|
|
|
|
public MetadataLoaderService(
|
|
IExperionOpcClient opcClient,
|
|
ExperionDbContext ctx,
|
|
ILogger<MetadataLoaderService> logger)
|
|
{
|
|
_opcClient = opcClient;
|
|
_ctx = ctx;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<int> LoadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string> baseTags)
|
|
{
|
|
var baseTagList = baseTags.ToList(); // 이중 열거 방지
|
|
|
|
// ── Step 1: 모든 노드 ID 수집 ──────────────────────────────────────
|
|
var nodeMap = new Dictionary<string, (string baseTag, string attr)>();
|
|
foreach (var baseTag in baseTagList)
|
|
{
|
|
foreach (var attr in MetaAttributes)
|
|
{
|
|
var nodeId = $"{cfg.ServerHostName}:{baseTag}.{attr}";
|
|
var fullNodeId = $"ns=1;s={nodeId}";
|
|
nodeMap[fullNodeId] = (baseTag.ToLowerInvariant(), attr);
|
|
}
|
|
}
|
|
|
|
// ── Step 2: 배치 읽기 (ReadTagsAsync 사용) ────────────────────────
|
|
var results = await _opcClient.ReadTagsAsync(cfg, nodeMap.Keys);
|
|
var entries = new List<(string baseTag, string attr, string? value, string nodeId)>();
|
|
|
|
foreach (var result in results)
|
|
{
|
|
if (result.Success && result.Value != null && nodeMap.TryGetValue(result.NodeId, out var meta))
|
|
{
|
|
entries.Add((meta.baseTag, meta.attr, result.Value?.ToString(), result.NodeId));
|
|
}
|
|
}
|
|
|
|
// ── Step 3: 단일 배치 UPSERT ──────────────────────────────────────
|
|
if (entries.Count > 0)
|
|
{
|
|
// VALUES 절을 동적으로 생성하여 한 번에 INSERT
|
|
// CTE 컬럼(5개)과 VALUES 값(5개) 일치: base_tag, attribute, value, node_id, loaded_at
|
|
var valuesSql = string.Join(", ", entries.Select((e, i) =>
|
|
$"(@bt{i}, @attr{i}, @val{i}, @nid{i}, NOW())"));
|
|
|
|
await _ctx.Database.ExecuteSqlRawAsync(@"
|
|
WITH new_data (base_tag, attribute, value, node_id, loaded_at) AS (
|
|
VALUES " + valuesSql + @"
|
|
)
|
|
INSERT INTO tag_metadata (base_tag, attribute, value, node_id, loaded_at)
|
|
SELECT base_tag, attribute, value, node_id, loaded_at FROM new_data
|
|
ON CONFLICT (base_tag, attribute)
|
|
DO UPDATE SET value = excluded.value, node_id = excluded.node_id, loaded_at = NOW()",
|
|
entries.SelectMany((e, i) => new object[] {
|
|
new NpgsqlParameter($"@bt{i}", e.baseTag),
|
|
new NpgsqlParameter($"@attr{i}", e.attr),
|
|
new NpgsqlParameter($"@val{i}", (object?)e.value ?? DBNull.Value),
|
|
new NpgsqlParameter($"@nid{i}", e.nodeId)
|
|
}).ToArray());
|
|
}
|
|
|
|
// v_tag_summary는 일반 VIEW이므로 REFRESH 불필요 (조회 시 실시간 JOIN)
|
|
_logger.LogInformation("[Metadata] 로드 완료: {Count}개 속성 ({TagCount}개 태그)", entries.Count, baseTagList.Count);
|
|
return entries.Count;
|
|
}
|
|
|
|
public async Task<int> ReloadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string>? baseTags = null)
|
|
{
|
|
// baseTags가 null이면 tag_metadata에서 전체 base_tag 조회
|
|
var tags = baseTags?.ToList() ?? await _ctx.TagMetadata.Select(t => t.BaseTag).Distinct().ToListAsync();
|
|
return await LoadMetadataAsync(cfg, tags);
|
|
}
|
|
}
|