feat: implementa campo Data_Hash per ottimizzazione trasferimenti
- Aggiunge colonna "Hash Dati" nella tabella delle associazioni con visualizzazione troncata - Implementa generazione hash SHA256 che include signature dei mapping per rilevare modifiche configurazione - Modifica logica trasferimento per saltare record con hash identico (ottimizzazione prestazioni) - Corregge UpdateAssociationAsync per persistere correttamente Data_Hash e LastVerifiedAt nel database - Aggiorna hash solo in caso di trasferimento riuscito, mantenendo coerenza tra Salesforce e database locale - Migliora logging per debug del sistema di hash e associazioni Risolve il problema dei trasferimenti continui quando i mapping cambiano e ottimizza le prestazioni saltando record non modificati.
This commit is contained in:
@@ -1727,6 +1727,7 @@ public partial class DataCoupler : ComponentBase
|
||||
"success" => "",
|
||||
"updated" => "table-info",
|
||||
"duplicate" => "table-warning",
|
||||
"skipped" => "table-secondary",
|
||||
"error" => "table-danger",
|
||||
_ => ""
|
||||
};
|
||||
@@ -1739,6 +1740,7 @@ public partial class DataCoupler : ComponentBase
|
||||
"success" => "bg-success",
|
||||
"updated" => "bg-info",
|
||||
"duplicate" => "bg-warning text-dark",
|
||||
"skipped" => "bg-secondary",
|
||||
"error" => "bg-danger",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
@@ -1751,6 +1753,7 @@ public partial class DataCoupler : ComponentBase
|
||||
"success" => "fa-check-circle",
|
||||
"updated" => "fa-edit",
|
||||
"duplicate" => "fa-exclamation-triangle",
|
||||
"skipped" => "fa-forward",
|
||||
"error" => "fa-times-circle",
|
||||
_ => "fa-question-circle"
|
||||
};
|
||||
@@ -1763,6 +1766,7 @@ public partial class DataCoupler : ComponentBase
|
||||
"success" => "Inserito",
|
||||
"updated" => "Aggiornato",
|
||||
"duplicate" => "Duplicato",
|
||||
"skipped" => "Saltato",
|
||||
"error" => "Errore",
|
||||
_ => "Sconosciuto"
|
||||
};
|
||||
@@ -1802,6 +1806,61 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera un hash SHA256 dei dati dei campi sorgente mappati.
|
||||
/// Utilizzato per rilevare cambiamenti nei dati e ottimizzare il trasferimento.
|
||||
/// Include anche una signature dei campi mappati per rilevare cambi di configurazione.
|
||||
/// </summary>
|
||||
private string GenerateDataHash(Dictionary<string, object> record)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Raccoglie i valori dei campi mappati in ordine alfabetico per garantire consistenza
|
||||
var mappedFields = fieldMappings.Keys.OrderBy(k => k).ToList();
|
||||
var valuesForHash = new List<string>();
|
||||
|
||||
// PRIMO: Aggiungi la signature dei mapping per rilevare cambi di configurazione
|
||||
var mappingSignature = string.Join(",", fieldMappings.OrderBy(m => m.Key).Select(m => $"{m.Key}->{m.Value}"));
|
||||
valuesForHash.Add($"MAPPING_SIGNATURE={mappingSignature}");
|
||||
|
||||
// SECONDO: Aggiungi i valori dei dati per ogni campo mappato
|
||||
foreach (var sourceField in mappedFields)
|
||||
{
|
||||
if (record.ContainsKey(sourceField))
|
||||
{
|
||||
var value = record[sourceField];
|
||||
var normalizedValue = value?.ToString()?.Trim() ?? "";
|
||||
valuesForHash.Add($"{sourceField}={normalizedValue}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Se il campo non è presente nel record, aggiungi una stringa vuota
|
||||
valuesForHash.Add($"{sourceField}=");
|
||||
}
|
||||
}
|
||||
|
||||
// Combina tutti i valori in una stringa unica
|
||||
var combinedData = string.Join("|", valuesForHash);
|
||||
|
||||
Logger.LogDebug("Hash dei dati generato da: {CombinedData}", combinedData);
|
||||
|
||||
// Calcola l'hash SHA256
|
||||
using (var sha256 = System.Security.Cryptography.SHA256.Create())
|
||||
{
|
||||
var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combinedData));
|
||||
var hashString = Convert.ToHexString(hashBytes);
|
||||
|
||||
Logger.LogDebug("Hash SHA256 generato: {Hash} (include signature mapping)", hashString);
|
||||
return hashString;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella generazione dell'hash dei dati");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce la connessione al database con schema specifico
|
||||
/// </summary>
|
||||
@@ -2313,19 +2372,6 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private async Task<string> GenerateUniqueProfileName(string baseName)
|
||||
{
|
||||
var uniqueName = baseName;
|
||||
@@ -2499,7 +2545,8 @@ public partial class DataCoupler : ComponentBase
|
||||
|
||||
// 2. Trasforma i record e analizza le associazioni IN PARALLELO
|
||||
var recordsForCreate = new ConcurrentBag<(Dictionary<string, object> transformedData, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordsForUpdate = new ConcurrentBag<(Dictionary<string, object> transformedData, string entityId, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordsForUpdate = new ConcurrentBag<(Dictionary<string, object> transformedData, string entityId, Dictionary<string, object> originalRecord, int recordNumber, string newDataHash)>();
|
||||
var recordsSkipped = new ConcurrentBag<(Dictionary<string, object> originalRecord, int recordNumber, string reason)>();
|
||||
var recordErrors = new ConcurrentBag<TransferResult>();
|
||||
|
||||
// Cattura i valori condivisi per evitare race conditions
|
||||
@@ -2524,10 +2571,11 @@ public partial class DataCoupler : ComponentBase
|
||||
// Trasforma il record in base ai mapping (operazione locale, thread-safe)
|
||||
var restData = TransformRecordToRestEntity(record);
|
||||
|
||||
// Genera la chiave sorgente per questo record (operazione locale, thread-safe)
|
||||
// Genera la chiave sorgente e l'hash dei dati per questo record (operazioni locali, thread-safe)
|
||||
var sourceKey = GenerateSourceKey(record);
|
||||
var currentDataHash = GenerateDataHash(record);
|
||||
|
||||
// Analizza le associazioni per capire se aggiornare o creare
|
||||
// Analizza le associazioni per capire se aggiornare, creare o saltare
|
||||
if (currentUseRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||
@@ -2554,14 +2602,27 @@ public partial class DataCoupler : ComponentBase
|
||||
|
||||
if (existingAssociation != null && existingAssociation.IsActive)
|
||||
{
|
||||
// Record da aggiornare
|
||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId})",
|
||||
recordNumber, existingAssociation.DestinationId);
|
||||
// CONTROLLO HASH: Verifica se i dati sono cambiati
|
||||
var existingHash = existingAssociation.Data_Hash;
|
||||
|
||||
if (!string.IsNullOrEmpty(existingHash) && existingHash.Equals(currentDataHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// I dati non sono cambiati, salta questo record
|
||||
recordsSkipped.Add((record, recordNumber, "Dati non modificati (hash identico)"));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} saltato - hash identico: {Hash}",
|
||||
recordNumber, currentDataHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
// I dati sono cambiati o l'hash è vuoto, procedi con l'aggiornamento
|
||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber, currentDataHash));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId}) - hash diverso: old={OldHash}, new={NewHash}",
|
||||
recordNumber, existingAssociation.DestinationId, existingHash ?? "NULL", currentDataHash);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record da creare
|
||||
// Record da creare (nessuna associazione esistente)
|
||||
recordsForCreate.Add((restData, record, recordNumber));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per creazione", recordNumber);
|
||||
}
|
||||
@@ -2601,9 +2662,22 @@ public partial class DataCoupler : ComponentBase
|
||||
// Converti i ConcurrentBag in liste per il resto del processing
|
||||
var finalRecordsForCreate = recordsForCreate.ToList();
|
||||
var finalRecordsForUpdate = recordsForUpdate.ToList();
|
||||
var finalRecordsSkipped = recordsSkipped.ToList();
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Analisi parallela completata in {ElapsedMs}ms - {CreateCount} record da creare, {UpdateCount} record da aggiornare, {ErrorCount} errori",
|
||||
analysisElapsed, finalRecordsForCreate.Count, finalRecordsForUpdate.Count, recordErrors.Count);
|
||||
Logger.LogInformation("COMPOSITE: Analisi parallela completata in {ElapsedMs}ms - {CreateCount} record da creare, {UpdateCount} record da aggiornare, {SkippedCount} record saltati, {ErrorCount} errori",
|
||||
analysisElapsed, finalRecordsForCreate.Count, finalRecordsForUpdate.Count, finalRecordsSkipped.Count, recordErrors.Count);
|
||||
|
||||
// Aggiungi i record saltati ai risultati di trasferimento
|
||||
foreach (var skipped in finalRecordsSkipped)
|
||||
{
|
||||
transferResults.Add(new TransferResult
|
||||
{
|
||||
RecordNumber = skipped.recordNumber,
|
||||
RecordData = skipped.originalRecord,
|
||||
Status = "skipped",
|
||||
Message = skipped.reason
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Esegui le chiamate composite in parallelo
|
||||
var createTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
||||
@@ -2659,7 +2733,9 @@ public partial class DataCoupler : ComponentBase
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId))
|
||||
{
|
||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||
var associationTask = CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber);
|
||||
// Genera l'hash per questo record per salvarlo nell'associazione
|
||||
var dataHashForAssociation = GenerateDataHash(originalData.originalRecord);
|
||||
var associationTask = CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber, dataHashForAssociation);
|
||||
createAssociationTasks.Add(associationTask);
|
||||
}
|
||||
}
|
||||
@@ -2699,8 +2775,8 @@ public partial class DataCoupler : ComponentBase
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId))
|
||||
{
|
||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||
var verificationTask = UpdateAssociationVerificationAsync(result.EntityId);
|
||||
updateAssociationTasks.Add(verificationTask);
|
||||
var updateHashTask = UpdateAssociationHashAsync(originalData.originalRecord, result.EntityId, originalData.newDataHash);
|
||||
updateAssociationTasks.Add(updateHashTask);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2709,13 +2785,9 @@ public partial class DataCoupler : ComponentBase
|
||||
transferResult.Status = "error";
|
||||
transferResult.Message = $"Errore aggiornamento (Composite): {result.ErrorMessage}";
|
||||
|
||||
// Aggiungi task di gestione fallimento alla lista (esecuzione parallela)
|
||||
if (useRecordAssociations)
|
||||
{
|
||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||
var failureTask = HandleFailedUpdateAsync(originalData.originalRecord, originalData.recordNumber);
|
||||
updateAssociationTasks.Add(failureTask);
|
||||
}
|
||||
// NON aggiornare l'hash in caso di errore nel trasferimento
|
||||
Logger.LogWarning("COMPOSITE: Trasferimento fallito per record {RecordNumber} - EntityId: {EntityId}. Hash non aggiornato.",
|
||||
originalData.recordNumber, result.EntityId ?? "N/A");
|
||||
}
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
@@ -2740,11 +2812,12 @@ public partial class DataCoupler : ComponentBase
|
||||
Logger.LogInformation("COMPOSITE: Nessuna operazione di associazione da eseguire");
|
||||
}
|
||||
|
||||
// 7. Mostra risultati
|
||||
ShowTransferResults(successCount, updatedCount, 0, errorCount);
|
||||
// 7. Mostra risultati (inclusi i record saltati)
|
||||
var skippedCount = finalRecordsSkipped.Count;
|
||||
ShowTransferResults(successCount, updatedCount, 0, errorCount, skippedCount);
|
||||
|
||||
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Errori: {ErrorCount}",
|
||||
successCount, updatedCount, errorCount);
|
||||
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Saltati: {SkippedCount}, Errori: {ErrorCount}",
|
||||
successCount, updatedCount, skippedCount, errorCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2758,7 +2831,7 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateAssociationAsync(Dictionary<string, object> originalRecord, string entityId, int recordNumber)
|
||||
private async Task CreateAssociationAsync(Dictionary<string, object> originalRecord, string entityId, int recordNumber, string? dataHash = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -2772,6 +2845,9 @@ public partial class DataCoupler : ComponentBase
|
||||
var sourceKey = GenerateSourceKey(originalRecord);
|
||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||
|
||||
// Usa l'hash passato come parametro o genera uno nuovo se non fornito
|
||||
var finalDataHash = dataHash ?? GenerateDataHash(originalRecord);
|
||||
|
||||
var destinationKeyField = GetEntityIdField();
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
@@ -2783,18 +2859,21 @@ public partial class DataCoupler : ComponentBase
|
||||
RestCredentialName = currentCredentialName,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastVerifiedAt = DateTime.UtcNow,
|
||||
Data_Hash = finalDataHash,
|
||||
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
TransferDate = DateTime.UtcNow,
|
||||
RecordNumber = recordNumber,
|
||||
MappingCount = currentMappingCount,
|
||||
SourceType = currentSourceType,
|
||||
CompositeTransfer = true
|
||||
CompositeTransfer = true,
|
||||
DataHashGenerated = true
|
||||
})
|
||||
};
|
||||
|
||||
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(association);
|
||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL)", associationId, recordNumber);
|
||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL) - Hash: {Hash}",
|
||||
associationId, recordNumber, finalDataHash);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2802,6 +2881,43 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAssociationHashAsync(Dictionary<string, object> originalRecord, string entityId, string newDataHash)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Cattura i valori condivisi per evitare race conditions
|
||||
var currentEntityName = selectedRestEntity?.Name ?? "";
|
||||
var currentCredentialName = selectedRestCredential ?? "";
|
||||
|
||||
var sourceKey = GenerateSourceKey(originalRecord);
|
||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||
|
||||
// Trova l'associazione esistente e aggiorna l'hash
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
|
||||
sourceKey, currentEntityName, currentCredentialName);
|
||||
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
existingAssociation.Data_Hash = newDataHash;
|
||||
existingAssociation.LastVerifiedAt = DateTime.UtcNow;
|
||||
existingAssociation.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await CredentialService.UpdateKeyAssociationAsync(existingAssociation);
|
||||
Logger.LogDebug("COMPOSITE: Hash associazione aggiornato per entityId {EntityId} - Nuovo hash: {Hash}",
|
||||
entityId, newDataHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("COMPOSITE: Associazione non trovata per aggiornamento hash - EntityId: {EntityId}, SourceKey: {SourceKey}",
|
||||
entityId, sourceKey);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Errore nell'aggiornamento dell'hash dell'associazione per entityId {EntityId}", entityId);
|
||||
}
|
||||
}
|
||||
|
||||
private Task UpdateAssociationVerificationAsync(string entityId)
|
||||
{
|
||||
try
|
||||
@@ -2840,7 +2956,7 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTransferResults(int successCount, int updatedCount, int duplicateCount, int errorCount)
|
||||
private void ShowTransferResults(int successCount, int updatedCount, int duplicateCount, int errorCount, int skippedCount = 0)
|
||||
{
|
||||
if (errorCount == 0)
|
||||
{
|
||||
@@ -2849,6 +2965,7 @@ public partial class DataCoupler : ComponentBase
|
||||
|
||||
if (successCount > 0) messageParts.Add($"{successCount} record inseriti");
|
||||
if (updatedCount > 0) messageParts.Add($"{updatedCount} record aggiornati");
|
||||
if (skippedCount > 0) messageParts.Add($"{skippedCount} record saltati (dati non modificati)");
|
||||
if (duplicateCount > 0) messageParts.Add($"{duplicateCount} duplicati rilevati (warning)");
|
||||
|
||||
message += string.Join(", ", messageParts) + ".";
|
||||
@@ -2862,6 +2979,7 @@ public partial class DataCoupler : ComponentBase
|
||||
|
||||
if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}");
|
||||
if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}");
|
||||
if (skippedCount > 0) messageParts.Add($"Saltati: {skippedCount}");
|
||||
if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}");
|
||||
messageParts.Add($"Errori: {errorCount}");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user