feat: Implementato sistema di associazioni chiave per prevenire duplicati nel data coupling
BREAKING CHANGE: Rimosso completamente il vecchio sistema RecordAssociation Modifiche principali: - Sostituito RecordAssociation con KeyAssociation basato sui valori delle chiavi - Implementata logica robusta di UPDATE vs INSERT basata su associazioni esistenti - Aggiunta normalizzazione delle chiavi (.Trim()) per consistenza - Implementato fallback nella ricerca associazioni per maggiore affidabilità - Sostituita verifica pre-UPDATE con tentativo diretto più efficiente Componenti modificati: - Nuovo modello: KeyAssociation.cs con campi ottimizzati - Nuovo servizio: KeyAssociationService.cs con metodi completi - Aggiornato: DataCoupler.razor con logica migliorata di gestione associazioni - Aggiornato: CredentialDbContext per gestire solo KeyAssociations - Aggiornati: tutti i servizi di interfaccia per supportare il nuovo sistema - Creata: pagina KeyAssociations.razor per gestione associazioni - Aggiornato: NavMenu.razor con link alla gestione associazioni Miglioramenti tecnici: - Logica di UPDATE più robusta: tenta direttamente l'aggiornamento invece di verificare prima l'esistenza - Gestione errori migliorata con cleanup automatico delle associazioni non valide - Debug logging estensivo per troubleshooting - Fallback nella ricerca associazioni se parametri specifici falliscono - Normalizzazione valori chiave per prevenire problemi di whitespace Risultato: Il sistema ora previene correttamente i duplicati utilizzando le associazioni per decidere se fare INSERT (nuovo record) o UPDATE (record esistente) basandosi sui valori delle chiavi. Database: - Creata migrazione EF per rimuovere RecordAssociations e aggiungere KeyAssociations - Eliminati file e codice legacy non più necessari
This commit is contained in:
@@ -834,13 +834,24 @@
|
||||
<input class="form-check-input" type="checkbox" id="useAssociations"
|
||||
@bind="useRecordAssociations" />
|
||||
<label class="form-check-label" for="useAssociations">
|
||||
<strong>Utilizza sistema di associazioni per tracking automatico degli aggiornamenti</strong>
|
||||
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra record sorgente e destinazione</small>
|
||||
<strong>Utilizza sistema di associazioni basato sui valori delle chiavi</strong>
|
||||
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra valori chiave e record di destinazione, permettendo aggiornamenti indipendentemente dalla sorgente</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (useRecordAssociations)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
<strong>Come funziona il nuovo sistema:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li>Ogni valore di chiave univoco viene associato a un record di destinazione</li>
|
||||
<li>Più sorgenti diverse possono gestire lo stesso oggetto business usando lo stesso valore chiave</li>
|
||||
<li>Gli aggiornamenti avvengono automaticamente quando si trova un'associazione esistente</li>
|
||||
<li>Il sistema individua automaticamente le chiavi dove possibile, ma puoi sempre scegliere manualmente</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Campo Chiave Sorgente: <span class="text-danger">*</span></label>
|
||||
@@ -2126,79 +2137,101 @@
|
||||
|
||||
// Genera la chiave sorgente per questo record
|
||||
var sourceKey = GenerateSourceKey(record);
|
||||
var currentSourceName = selectedSourceType == "database"
|
||||
? (useCustomQuery ? "custom_query" : selectedTable)
|
||||
: selectedSheet;
|
||||
|
||||
// NUOVA LOGICA: Cerca associazione esistente
|
||||
// NUOVO SISTEMA: Cerca associazione esistente basata sul valore della chiave
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
var existingAssociation = await CredentialService.FindRecordAssociationAsync(
|
||||
currentSourceName, sourceKey, selectedRestEntity.Name);
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Cerco associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||
|
||||
// Cerca se esiste già un'associazione per questo valore chiave
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||
|
||||
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
|
||||
if (existingAssociation == null)
|
||||
{
|
||||
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non trovata con parametri specifici, provo solo con KeyValue: '{KeyValue}'", sourceKey);
|
||||
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
|
||||
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
Logger.LogWarning("ASSOCIATION DEBUG: Trovata associazione con fallback - ID: {AssociationId}, Entity: '{Entity}', Credential: '{Credential}'",
|
||||
existingAssociation.Id, existingAssociation.DestinationEntity, existingAssociation.RestCredentialName);
|
||||
|
||||
// Verifica se l'associazione trovata è compatibile
|
||||
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
|
||||
existingAssociation.RestCredentialName != selectedRestCredential)
|
||||
{
|
||||
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non compatibile - Entity: '{FoundEntity}' vs '{ExpectedEntity}', Credential: '{FoundCredential}' vs '{ExpectedCredential}'",
|
||||
existingAssociation.DestinationEntity, selectedRestEntity.Name, existingAssociation.RestCredentialName, selectedRestCredential);
|
||||
existingAssociation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Associazione finale: {Found}. ID: {AssociationId}, DestinationId: '{DestinationId}', IsActive: {IsActive}",
|
||||
existingAssociation != null, existingAssociation?.Id, existingAssociation?.DestinationId, existingAssociation?.IsActive);
|
||||
|
||||
if (existingAssociation != null && existingAssociation.IsActive)
|
||||
{
|
||||
// VALIDAZIONE: Verifica se l'ID di destinazione esiste ancora nel sistema target
|
||||
bool destinationExists = false;
|
||||
// Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId);
|
||||
|
||||
try
|
||||
{
|
||||
// Usa il campo ID appropriato per cercare l'entità
|
||||
var idField = GetEntityIdField(); // Potrebbe essere "DocEntry", "id", "Id", etc.
|
||||
var searchKeys = new Dictionary<string, object> { { idField, existingAssociation.DestinationId } };
|
||||
|
||||
var foundEntities = await currentRestClient.FindEntitiesByKeysAsync(
|
||||
selectedRestEntity.Name, searchKeys);
|
||||
destinationExists = foundEntities != null && foundEntities.Any();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Errore nella verifica dell'esistenza dell'entità {EntityId} - assumo che non esista", existingAssociation.DestinationId);
|
||||
destinationExists = false;
|
||||
}
|
||||
|
||||
if (!destinationExists)
|
||||
{
|
||||
// L'ID di destinazione non esiste più - elimina l'associazione non valida
|
||||
Logger.LogWarning("ID destinazione {DestinationId} non più valido per associazione {AssociationId} - eliminazione associazione",
|
||||
existingAssociation.DestinationId, existingAssociation.Id);
|
||||
var updateResult = await currentRestClient.UpdateEntityAsync(
|
||||
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
|
||||
|
||||
await CredentialService.DeleteRecordAssociationAsync(existingAssociation.Id);
|
||||
|
||||
transferResult.Status = "error";
|
||||
transferResult.Message = $"Associazione non valida eliminata (ID destinazione {existingAssociation.DestinationId} non esiste più) - creazione nuovo record";
|
||||
|
||||
// Procedi con la creazione di un nuovo record
|
||||
goto CreateNewRecord;
|
||||
if (updateResult != null)
|
||||
{
|
||||
updatedCount++;
|
||||
transferResult.Status = "updated";
|
||||
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
|
||||
transferResult.EntityId = existingAssociation.DestinationId;
|
||||
|
||||
// Aggiorna l'associazione con la data di ultimo aggiornamento e verifica
|
||||
existingAssociation.UpdatedAt = DateTime.UtcNow;
|
||||
existingAssociation.LastVerifiedAt = DateTime.UtcNow;
|
||||
await CredentialService.UpdateKeyAssociationAsync(existingAssociation);
|
||||
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Record aggiornato con successo tramite associazione: {EntityId} per valore chiave {KeyValue}",
|
||||
existingAssociation.DestinationId, sourceKey);
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
recordNumber++;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update fallito ma senza eccezione - probabilmente l'entità non esiste più
|
||||
Logger.LogWarning("ASSOCIATION DEBUG: Aggiornamento fallito (result null) per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
|
||||
goto HandleInvalidAssociation;
|
||||
}
|
||||
}
|
||||
catch (Exception updateEx)
|
||||
{
|
||||
// Update fallito con eccezione - probabilmente l'entità non esiste più
|
||||
Logger.LogWarning(updateEx, "ASSOCIATION DEBUG: Aggiornamento fallito per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
|
||||
goto HandleInvalidAssociation;
|
||||
}
|
||||
|
||||
// L'ID di destinazione esiste - procedi con l'aggiornamento
|
||||
var updateResult = await currentRestClient.UpdateEntityAsync(
|
||||
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
|
||||
|
||||
if (updateResult != null)
|
||||
HandleInvalidAssociation:
|
||||
// L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida
|
||||
try
|
||||
{
|
||||
updatedCount++;
|
||||
transferResult.Status = "updated";
|
||||
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
|
||||
transferResult.EntityId = existingAssociation.DestinationId;
|
||||
|
||||
// Aggiorna l'associazione con la data di ultimo aggiornamento
|
||||
existingAssociation.UpdatedAt = DateTime.UtcNow;
|
||||
await CredentialService.UpdateRecordAssociationAsync(existingAssociation);
|
||||
|
||||
Logger.LogDebug("Record aggiornato tramite associazione: {EntityId} per chiave sorgente {SourceKey}",
|
||||
existingAssociation.DestinationId, sourceKey);
|
||||
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id);
|
||||
}
|
||||
else
|
||||
catch (Exception delEx)
|
||||
{
|
||||
// Se l'aggiornamento fallisce, prova a creare un nuovo record
|
||||
Logger.LogWarning("Aggiornamento fallito per associazione {AssociationId}, provo a creare nuovo record", existingAssociation.Id);
|
||||
goto CreateNewRecord;
|
||||
Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id);
|
||||
}
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
recordNumber++;
|
||||
continue;
|
||||
transferResult.Status = "info";
|
||||
transferResult.Message = $"Associazione non valida eliminata (aggiornamento fallito) - creazione nuovo record";
|
||||
|
||||
// Procedi con la creazione di un nuovo record (non aggiungere il result qui, sarà aggiunto dopo CreateNewRecord)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2212,31 +2245,41 @@
|
||||
transferResult.Status = "success";
|
||||
transferResult.Message = "Record inserito con successo";
|
||||
transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() :
|
||||
result.ContainsKey("Id") ? result["Id"]?.ToString() : null;
|
||||
result.ContainsKey("Id") ? result["Id"]?.ToString() :
|
||||
result.ContainsKey("DocEntry") ? result["DocEntry"]?.ToString() : null;
|
||||
|
||||
// Crea associazione solo se abbiamo una chiave sorgente e un ID destinazione
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var association = new RecordAssociation
|
||||
// Determina i campi chiave automaticamente
|
||||
var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione
|
||||
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
SourceName = currentSourceName,
|
||||
SourceType = selectedSourceType,
|
||||
SourceKey = sourceKey,
|
||||
KeyValue = sourceKey,
|
||||
SourceKeyField = sourceKeyField,
|
||||
DestinationKeyField = destinationKeyField,
|
||||
DestinationEntity = selectedRestEntity.Name,
|
||||
DestinationId = transferResult.EntityId,
|
||||
RestCredentialName = selectedRestCredential,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastVerifiedAt = DateTime.UtcNow,
|
||||
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
TransferDate = DateTime.UtcNow,
|
||||
RecordNumber = recordNumber,
|
||||
MappingCount = fieldMappings.Count
|
||||
MappingCount = fieldMappings.Count,
|
||||
SourceType = selectedSourceType
|
||||
})
|
||||
};
|
||||
|
||||
await CredentialService.SaveRecordAssociationAsync(association);
|
||||
Logger.LogDebug("Associazione creata: {SourceKey} -> {DestinationId}", sourceKey, transferResult.EntityId);
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'",
|
||||
sourceKey, selectedRestEntity.Name, transferResult.EntityId, selectedRestCredential);
|
||||
|
||||
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
||||
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
|
||||
}
|
||||
catch (Exception assocEx)
|
||||
{
|
||||
@@ -2599,7 +2642,8 @@
|
||||
throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record.");
|
||||
}
|
||||
|
||||
return keyValue;
|
||||
// Normalizza il valore della chiave (trim e gestione case-sensitive)
|
||||
return keyValue.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user