[Feature/Perf] Ottimizzazioni bulk pre-discovery, batch deletion sync e supporto OLE DB / Salesforce client_credentials
## Bulk Pre-Discovery e riduzione query SQLite/SOQL
### KeyAssociationService — FindAssociationsByKeyValuesBulkAsync (nuovo)
- Aggiunta query bulk 'WHERE KeyValue IN (...)' per recuperare N associazioni con 1 sola query SQLite
(chunking a 500 chiavi per rispettare il limite ~999 parametri di SQLite)
- Aggiunta interfaccia IKeyAssociationService e delegata in DataConnectionCredentialService / IDataConnectionCredentialService
### AssociationService — BatchFindOrCreateAssociationsAsync (nuovo)
- Nuovo metodo bulk che sostituisce i loop per-record durante l'analisi composite:
1) 1 query SQLite bulk per tutte le chiavi
2) Per le chiavi non trovate: SOQL 'IN (...)' su Salesforce in chunk da 200 via BatchExecuteQueriesAsync
(ceil(K/25) HTTP Composite call invece di K singole)
3) Salvataggio parallelo delle associazioni pre-discovery scoperte
- Fallback per-record automatico per client REST non Salesforce
- Aggiornata interfaccia IAssociationService con documentazione XML completa
### DataCoupler.razor.cs — STEP A/B nel flusso COMPOSITE
- Pre-Discovery spostata FUORI dal loop parallelo (STEP A, prima dell'analisi)
- associationsByKey pre-popolato con BatchFindOrCreateAssociationsAsync
- STEP B: il loop parallelo usa TryGetValue O(1) invece di query async per record
- Rimozione blocco ~40 righe di per-record lookup / fallback duplicati
## Salesforce Composite API — Batch Delete e Patch
### SalesforceServiceClient — metodi batch (nuovi)
- BatchDeleteEntitiesAsync: elimina N record con ceil(N/25) Composite call invece di N
- BatchPatchSingleFieldAsync: aggiorna un singolo campo su N record tramite BatchUpdateEntitiesAsync
### DeletionSyncService — refactoring batch
- ExecuteBatchedSalesforceDeletionsAsync: orchestrazione batch per Delete / Deactivate / Mark su Salesforce
- ExecuteSequentialDeletionsAsync: loop sequenziale esistente estratto in metodo riutilizzabile
- Dispatcher: Salesforce -> batch Composite, altri client REST -> sequenziale
## Supporto OLE DB (database)
### DatabaseSchemaProviderFactory
- Aggiunto case DatabaseType.OleDb -> new OleDbSchemaProvider() nel factory switch
### DatabaseMethod.cs
- Aggiunto metodo IsOleDbConnection() (parallelo a IsOdbcConnection())
- Query validation e manager temporaneo estesi a OLE DB oltre che ODBC
- GetLimitedQuery: aggiunto case OleDb -> 'SELECT TOP N FROM (subquery)'
## Salesforce OAuth2 — fix client_credentials
### CredentialService.cs
- Aggiunto 'GrantType' alla HashSet serviceSpecificKeys per preservarlo nella serializzazione AdditionalParameters
### DataConnectionCredentialService.cs
- Refactored BuildRestServiceOptions in helper statico riutilizzato da entrambi i metodi GetRestServiceOptions
- Mapping coerente ClientId/ClientSecret/GrantType per Salesforce (allineato a DataConnectionFactory)
- TestSalesforceOAuthLogin: branch esplicito per client_credentials (no username/password/token)
con validazione preventiva ClientId+ClientSecret obbligatori
- Log flow label (password|client_credentials) in tutti i messaggi di autenticazione
## VS Code tasks
### .vscode/tasks.json
- Rimosso task generico 'Publish Data_Coupler'
- Aggiunti due task separati: win-x64 e win-x86, entrambi SingleFile + Self-Contained + ReadyToRun
This commit is contained in:
@@ -3491,10 +3491,42 @@ public partial class DataCoupler : ComponentBase
|
||||
// Crea lista indicizzata per mantenere il record number
|
||||
var indexedRecords = records.Select((record, index) => new { Record = record, RecordNumber = index + 1 }).ToList();
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Inizio analisi parallela di {RecordCount} record", indexedRecords.Count);
|
||||
Logger.LogInformation("COMPOSITE: Inizio analisi di {RecordCount} record", indexedRecords.Count);
|
||||
var analysisStartTime = DateTime.UtcNow;
|
||||
|
||||
// Processa tutti i record in parallelo
|
||||
// === STEP A: Bulk Pre-Discovery (1 query SQLite + poche SOQL IN invece di 2N+N) ===
|
||||
var sourceKeysForBulk = new List<string>(indexedRecords.Count);
|
||||
foreach (var idx in indexedRecords)
|
||||
{
|
||||
var key = GenerateSourceKey(idx.Record);
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
sourceKeysForBulk.Add(key);
|
||||
}
|
||||
|
||||
Dictionary<string, KeyAssociation> associationsByKey = new(StringComparer.Ordinal);
|
||||
if (currentUseRecordAssociations && !string.IsNullOrEmpty(currentSourceKeyField) && sourceKeysForBulk.Count > 0)
|
||||
{
|
||||
var commonRequest = new PreDiscoveryRequest
|
||||
{
|
||||
SourceKeyField = currentSourceKeyField,
|
||||
DestinationEntity = currentEntityName,
|
||||
CredentialName = currentCredentialName,
|
||||
DestinationKeyField = GetEntityIdField(),
|
||||
FieldMappings = currentFieldMappings,
|
||||
RestClient = currentRestClient,
|
||||
EnablePreDiscovery = true,
|
||||
UseParallelMethod = true,
|
||||
IsScheduledTransfer = false
|
||||
};
|
||||
|
||||
associationsByKey = await AssociationService.BatchFindOrCreateAssociationsAsync(
|
||||
sourceKeysForBulk, commonRequest);
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Bulk Pre-Discovery completata - {Found}/{Total} associazioni risolte",
|
||||
associationsByKey.Count, sourceKeysForBulk.Count);
|
||||
}
|
||||
|
||||
// === STEP B: Analisi locale parallela (no I/O) ===
|
||||
var processingTasks = indexedRecords.Select(async indexedRecord =>
|
||||
{
|
||||
try
|
||||
@@ -3513,48 +3545,7 @@ public partial class DataCoupler : ComponentBase
|
||||
// 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}'",
|
||||
sourceKey, currentEntityName, currentCredentialName);
|
||||
|
||||
// Usa i metodi paralleli per le operazioni di database
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
|
||||
sourceKey, currentEntityName, currentCredentialName);
|
||||
|
||||
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
|
||||
if (existingAssociation == null)
|
||||
{
|
||||
existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(sourceKey);
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
// Verifica compatibilità
|
||||
if (existingAssociation.DestinationEntity != currentEntityName ||
|
||||
existingAssociation.RestCredentialName != currentCredentialName)
|
||||
{
|
||||
existingAssociation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔍 PRE-DISCOVERY: Usa il servizio centralizzato
|
||||
if (existingAssociation == null)
|
||||
{
|
||||
var preDiscoveryRequest = new PreDiscoveryRequest
|
||||
{
|
||||
SourceKey = sourceKey,
|
||||
SourceKeyField = currentSourceKeyField,
|
||||
DestinationEntity = currentEntityName,
|
||||
CredentialName = currentCredentialName,
|
||||
DestinationKeyField = GetEntityIdField(),
|
||||
FieldMappings = currentFieldMappings,
|
||||
RestClient = currentRestClient,
|
||||
CurrentDataHash = currentDataHash,
|
||||
EnablePreDiscovery = true,
|
||||
UseParallelMethod = true, // Usa metodi paralleli thread-safe
|
||||
IsScheduledTransfer = false
|
||||
};
|
||||
|
||||
existingAssociation = await AssociationService.FindOrCreateAssociationAsync(preDiscoveryRequest);
|
||||
}
|
||||
associationsByKey.TryGetValue(sourceKey, out var existingAssociation);
|
||||
|
||||
if (existingAssociation != null && existingAssociation.IsActive)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user