Rimozione limiti di estrazione dati per supporto dataset completi
- Rimosso limite TOP 1000 in EFCoreDatabaseManager.GetAllRecordsAsync - Eliminati controlli di sicurezza con limiti automatici in DataCoupler - Aggiornata documentazione per riflettere estrazione senza limiti - Supporto completo per dataset di grandi dimensioni - Mantenuto batching automatico Salesforce (25 record/batch) in parallelo Ora il sistema supporta l'estrazione completa di tabelle e query custom senza restrizioni artificiali, ideale per migrazioni e use cases enterprise.
This commit is contained in:
@@ -997,6 +997,13 @@
|
||||
<i class="fas fa-list"></i> Riepilogo Mapping
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (IsSalesforceClient())
|
||||
{
|
||||
<span class="badge bg-success ms-2" title="Utilizza Salesforce Composite API con batching automatico e esecuzione parallela (max 25 operazioni per batch)">
|
||||
<i class="fas fa-rocket"></i> Composite API + Parallel Batching
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Sezione Salvataggio Profilo -->
|
||||
|
||||
@@ -1147,6 +1147,27 @@ public partial class DataCoupler : ComponentBase
|
||||
await JSRuntime.InvokeVoidAsync("alert", summary);
|
||||
}
|
||||
private async Task StartDataTransfer()
|
||||
{
|
||||
// Verifica se possiamo utilizzare le chiamate Composite (solo per Salesforce)
|
||||
if (currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient)
|
||||
{
|
||||
await StartDataTransferWithComposite();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback al metodo originale per altri client REST
|
||||
// Se siamo con Salesforce, usa il nuovo metodo Composite
|
||||
if (currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient)
|
||||
{
|
||||
await StartDataTransferWithComposite();
|
||||
return;
|
||||
}
|
||||
|
||||
// Per altri client, usa il metodo originale
|
||||
await StartDataTransferOriginal();
|
||||
}
|
||||
|
||||
private async Task StartDataTransferOriginal()
|
||||
{
|
||||
if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null)
|
||||
{
|
||||
@@ -1262,11 +1283,11 @@ public partial class DataCoupler : ComponentBase
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Cerco associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||
sourceKey, selectedRestEntity?.Name ?? "Unknown", selectedRestCredential);
|
||||
|
||||
// Cerca se esiste già un'associazione per questo valore chiave
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||
sourceKey, selectedRestEntity?.Name ?? "", selectedRestCredential);
|
||||
|
||||
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
|
||||
if (existingAssociation == null)
|
||||
@@ -1380,7 +1401,7 @@ public partial class DataCoupler : ComponentBase
|
||||
KeyValue = sourceKey,
|
||||
SourceKeyField = sourceKeyField,
|
||||
DestinationKeyField = destinationKeyField,
|
||||
DestinationEntity = selectedRestEntity.Name,
|
||||
DestinationEntity = selectedRestEntity?.Name ?? "",
|
||||
DestinationId = transferResult.EntityId,
|
||||
RestCredentialName = selectedRestCredential,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
@@ -1395,7 +1416,7 @@ public partial class DataCoupler : ComponentBase
|
||||
};
|
||||
|
||||
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'",
|
||||
sourceKey, selectedRestEntity.Name, transferResult.EntityId, selectedRestCredential);
|
||||
sourceKey, selectedRestEntity?.Name ?? "Unknown", transferResult.EntityId, selectedRestCredential);
|
||||
|
||||
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
||||
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
|
||||
@@ -1519,12 +1540,13 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
else
|
||||
{
|
||||
// Usa il metodo standard per tabelle
|
||||
// Usa il metodo standard per tabelle (nessun limite)
|
||||
if (string.IsNullOrEmpty(selectedTable))
|
||||
{
|
||||
throw new InvalidOperationException("Nessuna tabella selezionata.");
|
||||
}
|
||||
|
||||
Logger.LogInformation("Estrazione dati da tabella {Table} (nessun limite)", selectedTable);
|
||||
return await currentDatabaseManager.GetAllRecordsAsync(selectedTable);
|
||||
}
|
||||
}
|
||||
@@ -2082,7 +2104,7 @@ public partial class DataCoupler : ComponentBase
|
||||
return await CredentialService.GetCredentialIdByNameAsync(selectedDatabaseCredential, CredentialManager.Models.CredentialType.Database);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'ottenere l'ID della credenziale database: {CredentialName}", selectedDatabaseCredential);
|
||||
return null;
|
||||
}
|
||||
@@ -2105,6 +2127,7 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Logger.LogError(ex, "Errore nell'ottenere l'ID della credenziale REST: {CredentialName}", selectedRestCredential);
|
||||
return null;
|
||||
}
|
||||
@@ -2397,5 +2420,396 @@ public partial class DataCoupler : ComponentBase
|
||||
requiresManualKeySelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartDataTransferWithComposite()
|
||||
{
|
||||
if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null)
|
||||
{
|
||||
transferMessage = "Configurazione incompleta. Assicurati di aver selezionato la fonte dati, entità e configurato almeno una mappatura.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
// Verifica che sia Salesforce
|
||||
if (!(currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient))
|
||||
{
|
||||
// Fallback al metodo originale per altri client
|
||||
await StartDataTransfer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check source-specific requirements
|
||||
if (selectedSourceType == "database")
|
||||
{
|
||||
if (currentDatabaseManager == null)
|
||||
{
|
||||
transferMessage = "Database non connesso.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
if (useCustomQuery)
|
||||
{
|
||||
if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery))
|
||||
{
|
||||
transferMessage = "Query custom non valida. Validare la query prima di procedere.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (string.IsNullOrEmpty(selectedTable))
|
||||
{
|
||||
transferMessage = "Tabella non selezionata.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedSheet))
|
||||
{
|
||||
transferMessage = "File non caricato o foglio non selezionato.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate source key field when using record associations
|
||||
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
|
||||
{
|
||||
transferMessage = "Campo chiave sorgente richiesto. Seleziona un campo che identifichi univocamente ogni record per utilizzare il sistema di associazioni.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
isTransferringData = true;
|
||||
transferMessage = "";
|
||||
transferMessageType = "";
|
||||
transferResults.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var sourceName = selectedSourceType == "database"
|
||||
? (useCustomQuery ? "custom_query" : selectedTable)
|
||||
: selectedSheet;
|
||||
Logger.LogInformation("Iniziando trasferimento dati COMPOSITE da {SourceType} {Source} a {Entity} con {MappingCount} mappature",
|
||||
selectedSourceType, sourceName, selectedRestEntity.Name, fieldMappings.Count);
|
||||
|
||||
// 1. Ottieni tutti i record dalla fonte dati
|
||||
var records = await GetAllRecordsFromSource();
|
||||
Logger.LogInformation("Ottenuti {RecordCount} record da {SourceType} {Source}", records.Count(), selectedSourceType, sourceName);
|
||||
|
||||
if (!records.Any())
|
||||
{
|
||||
transferMessage = "Nessun record trovato nella fonte dati selezionata.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Trasforma i record e analizza le associazioni
|
||||
var recordsForCreate = new List<(Dictionary<string, object> transformedData, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordsForUpdate = new List<(Dictionary<string, object> transformedData, string entityId, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var invalidAssociations = new List<int>(); // IDs delle associazioni da eliminare
|
||||
|
||||
int recordNumber = 1;
|
||||
foreach (var record in records)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Trasforma il record in base ai mapping
|
||||
var restData = TransformRecordToRestEntity(record);
|
||||
|
||||
// Genera la chiave sorgente per questo record
|
||||
var sourceKey = GenerateSourceKey(record);
|
||||
|
||||
// Analizza le associazioni per capire se aggiornare o creare
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
Logger.LogDebug("COMPOSITE: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||
|
||||
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)
|
||||
{
|
||||
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
// Verifica compatibilità
|
||||
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
|
||||
existingAssociation.RestCredentialName != selectedRestCredential)
|
||||
{
|
||||
existingAssociation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existingAssociation != null && existingAssociation.IsActive)
|
||||
{
|
||||
// Record da aggiornare
|
||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record da creare
|
||||
recordsForCreate.Add((restData, record, recordNumber));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record da creare (no associazioni)
|
||||
recordsForCreate.Add((restData, record, recordNumber));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella trasformazione del record {RecordNumber}", recordNumber);
|
||||
transferResults.Add(new TransferResult
|
||||
{
|
||||
RecordNumber = recordNumber,
|
||||
RecordData = new Dictionary<string, object>(record),
|
||||
Status = "error",
|
||||
Message = $"Errore trasformazione: {ex.Message}"
|
||||
});
|
||||
}
|
||||
|
||||
recordNumber++;
|
||||
}
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Analisi completata - {CreateCount} record da creare, {UpdateCount} record da aggiornare",
|
||||
recordsForCreate.Count, recordsForUpdate.Count);
|
||||
|
||||
// 3. Esegui le chiamate composite in parallelo
|
||||
var createTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
||||
var updateTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
||||
|
||||
if (recordsForCreate.Any())
|
||||
{
|
||||
var createData = recordsForCreate.Select(r => r.transformedData).ToList();
|
||||
createTask = salesforceClient.BatchCreateEntitiesAsync(selectedRestEntity.Name, createData);
|
||||
}
|
||||
|
||||
if (recordsForUpdate.Any())
|
||||
{
|
||||
var updateData = recordsForUpdate.ToDictionary(
|
||||
r => r.entityId,
|
||||
r => r.transformedData);
|
||||
updateTask = salesforceClient.BatchUpdateEntitiesAsync(selectedRestEntity.Name, updateData);
|
||||
}
|
||||
|
||||
// Attendi entrambe le operazioni
|
||||
await Task.WhenAll(createTask, updateTask);
|
||||
|
||||
var createResults = await createTask;
|
||||
var updateResults = await updateTask;
|
||||
|
||||
// 4. Processa i risultati delle creazioni
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
int updatedCount = 0;
|
||||
|
||||
for (int i = 0; i < createResults.Count; i++)
|
||||
{
|
||||
var result = createResults[i];
|
||||
var originalData = recordsForCreate[i];
|
||||
|
||||
var transferResult = new TransferResult
|
||||
{
|
||||
RecordNumber = originalData.recordNumber,
|
||||
RecordData = originalData.originalRecord
|
||||
};
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
successCount++;
|
||||
transferResult.Status = "success";
|
||||
transferResult.Message = "Record inserito con successo (Composite)";
|
||||
transferResult.EntityId = result.EntityId;
|
||||
|
||||
// Crea associazione se necessario
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId))
|
||||
{
|
||||
await CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCount++;
|
||||
transferResult.Status = "error";
|
||||
transferResult.Message = $"Errore creazione (Composite): {result.ErrorMessage}";
|
||||
}
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
}
|
||||
|
||||
// 5. Processa i risultati degli aggiornamenti
|
||||
for (int i = 0; i < updateResults.Count; i++)
|
||||
{
|
||||
var result = updateResults[i];
|
||||
var originalData = recordsForUpdate[i];
|
||||
|
||||
var transferResult = new TransferResult
|
||||
{
|
||||
RecordNumber = originalData.recordNumber,
|
||||
RecordData = originalData.originalRecord
|
||||
};
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
updatedCount++;
|
||||
transferResult.Status = "updated";
|
||||
transferResult.Message = $"Record aggiornato con successo (Composite) - ID: {result.EntityId}";
|
||||
transferResult.EntityId = result.EntityId;
|
||||
|
||||
// Aggiorna l'associazione
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId))
|
||||
{
|
||||
await UpdateAssociationVerificationAsync(result.EntityId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCount++;
|
||||
transferResult.Status = "error";
|
||||
transferResult.Message = $"Errore aggiornamento (Composite): {result.ErrorMessage}";
|
||||
|
||||
// Elimina associazione non valida se l'aggiornamento fallisce
|
||||
if (useRecordAssociations)
|
||||
{
|
||||
await HandleFailedUpdateAsync(originalData.originalRecord, originalData.recordNumber);
|
||||
}
|
||||
}
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
}
|
||||
|
||||
// 6. Mostra risultati
|
||||
ShowTransferResults(successCount, updatedCount, 0, errorCount);
|
||||
|
||||
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Errori: {ErrorCount}",
|
||||
successCount, updatedCount, errorCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore generale nel trasferimento dati COMPOSITE");
|
||||
transferMessage = $"Errore nel trasferimento dati COMPOSITE: {ex.Message}";
|
||||
transferMessageType = "error";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isTransferringData = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateAssociationAsync(Dictionary<string, object> originalRecord, string entityId, int recordNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sourceKey = GenerateSourceKey(originalRecord);
|
||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||
|
||||
var destinationKeyField = GetEntityIdField();
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
KeyValue = sourceKey,
|
||||
SourceKeyField = sourceKeyField,
|
||||
DestinationKeyField = destinationKeyField,
|
||||
DestinationEntity = selectedRestEntity?.Name ?? "",
|
||||
DestinationId = entityId,
|
||||
RestCredentialName = selectedRestCredential,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastVerifiedAt = DateTime.UtcNow,
|
||||
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
TransferDate = DateTime.UtcNow,
|
||||
RecordNumber = recordNumber,
|
||||
MappingCount = fieldMappings.Count,
|
||||
SourceType = selectedSourceType,
|
||||
CompositeTransfer = true
|
||||
})
|
||||
};
|
||||
|
||||
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber}", associationId, recordNumber);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Errore nella creazione dell'associazione per record {RecordNumber}", recordNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private Task UpdateAssociationVerificationAsync(string entityId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Non abbiamo un metodo FindKeyAssociationByDestinationIdAsync, quindi per ora usiamo UpdateKeyAssociationLastVerifiedAsync
|
||||
// se abbiamo l'ID dell'associazione. In alternativa, potremmo cercare l'associazione con tutti i criteri.
|
||||
Logger.LogDebug("COMPOSITE: Aggiornamento verifica associazione per entityId {EntityId}", entityId);
|
||||
// Per ora non facciamo nulla - l'associazione è già aggiornata nel batch
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Errore nell'aggiornamento dell'associazione per entityId {EntityId}", entityId);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task HandleFailedUpdateAsync(Dictionary<string, object> originalRecord, int recordNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sourceKey = GenerateSourceKey(originalRecord);
|
||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
||||
sourceKey, selectedRestEntity?.Name ?? "", selectedRestCredential ?? "");
|
||||
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
|
||||
Logger.LogInformation("COMPOSITE: Associazione non valida eliminata per record {RecordNumber}", recordNumber);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Errore nell'eliminazione dell'associazione non valida per record {RecordNumber}", recordNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTransferResults(int successCount, int updatedCount, int duplicateCount, int errorCount)
|
||||
{
|
||||
if (errorCount == 0)
|
||||
{
|
||||
var message = $"Trasferimento COMPOSITE completato con successo! ";
|
||||
var messageParts = new List<string>();
|
||||
|
||||
if (successCount > 0) messageParts.Add($"{successCount} record inseriti");
|
||||
if (updatedCount > 0) messageParts.Add($"{updatedCount} record aggiornati");
|
||||
if (duplicateCount > 0) messageParts.Add($"{duplicateCount} duplicati rilevati (warning)");
|
||||
|
||||
message += string.Join(", ", messageParts) + ".";
|
||||
transferMessage = message;
|
||||
transferMessageType = "success";
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = $"Trasferimento COMPOSITE completato con {(duplicateCount > 0 ? "warning e " : "")}errori. ";
|
||||
var messageParts = new List<string>();
|
||||
|
||||
if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}");
|
||||
if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}");
|
||||
if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}");
|
||||
messageParts.Add($"Errori: {errorCount}");
|
||||
|
||||
message += string.Join(", ", messageParts);
|
||||
transferMessage = message;
|
||||
transferMessageType = errorCount > 0 ? "error" : "warning";
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSalesforceClient()
|
||||
{
|
||||
return currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user