Inizia a digitare per cercare.

Salesforce Salesforce FFLib DML

Bulk DML Service Pattern: Operazioni DML Parziali in Salesforce

Documentazione tecnica completa del framework Bulk DML Service Pattern per Salesforce. Impara a realizzare operazioni DML resilienti con successo parziale usando un API familiare in stile Unit of Work.

25 min di lettura

Introduzione: Il problema dell’Unit of Work e delle Operazioni Parziali

Quando lavoriamo con fflib e il suo pattern Unit of Work, incontriamo una limitazione importante: fflib_SObjectUnitOfWork utilizza DML con allOrNone = true per impostazione predefinita. Ciò significa che se un singolo record fallisce la validazione in un batch di 200 record, l’intera transazione viene annullata e nessun record viene salvato.

Questo comportamento è eccellente per garantire l’integrità transazionale, ma ci sono scenari in cui è necessario un approccio diverso:

  • Elaborazione massiva dei dati: Quando si elaborano grandi volumi di dati e si desidera salvare i record validi anche se alcuni falliscono.
  • Migrazioni di dati: Nei processi di migrazione in cui alcuni record potrebbero fallire, ma non si vuole perdere tutti i progressi.
  • Integrazioni: Quando si ricevono dati esterni, alcuni dei quali potrebbero non essere validi, ma si desidera elaborare quelli validi.
  • Elaborazione batch: Nelle classi batch in cui si desidera continuare anche se alcuni record falliscono.

fflib non fornisce una soluzione nativa per questa casistica, poiché l’Unit of Work è progettato per operazioni transazionali complete in cui tutto o niente deve essere eseguito. Inoltre, una delle ragioni principali per la creazione di questo framework è fornire un sistema di mocking semplice per le operazioni in massa utilizzando lo stesso pattern di dependency injection di fflib, consentendo di scrivere veri test unitari senza la necessità di toccare il database.

Il Bulk DML Service Pattern risolve questi problemi fornendo un’API familiare in stile Unit of Work ma con supporto per operazioni parziali utilizzando allOrNone = false, e un sistema di mocking completo che si integra perfettamente con la Application factory di fflib.

Che cos’è il Bulk DML Service Pattern?

Il Bulk DML Service Pattern è un framework progettato per gestire le operazioni DML (insert, update, upsert) in Salesforce con successo parziale (allOrNone = false). Fornisce un’API simile a fflib_SObjectUnitOfWork ma con la capacità di elaborare i record in modo che i singoli fallimenti non blocchino l’intera operazione.

Prerequisiti

⚠️ IMPORTANTE: Questo framework richiede che il tuo progetto utilizzi fflib come base architetturale. Il Bulk DML Service Pattern è progettato per integrarsi con la Application factory di fflib, utilizzando lo stesso pattern di dependency injection per facilitare il mocking e i test unitari.

Caratteristiche Chiave

  • API familiare: Metodi simili a Unit of Work (registerNew, registerUpdate, registerUpsert, commitWork)
  • Operazioni parziali: Consente ad alcuni record di fallire senza influenzare gli altri.
  • Risultati dettagliati: Fornisce un riepilogo completo di tutte le operazioni eseguite.
  • Logging integrato: Registra automaticamente i fallimenti e i successi (utilizza il tuo sistema di logging preferito).
  • Testabile: Include un mock completo per test unitari senza database che si integra con fflib.
  • Gestione degli errori: Eccezioni personalizzate e logging strutturato.
  • Integrazione con fflib: Utilizza la Application factory di fflib per dependency injection e mocking.

Architettura del Framework

Il framework è costruito su un’architettura basata su interfacce che consente un facile mocking per il testing:

IBulkDmlService (Interfaccia)

    ├── BulkDmlService (Implementazione reale)
    └── BulkDmlServiceMock (Mock per il testing)

IBulkDmlResult (Interfaccia)

    └── BulkDmlResult (Implementazione)

Flusso di Operazione

graph TD
    A[Crea istanza di BulkDmlService] --> B[Registra record con registerNew/Update/Upsert]
    B --> C[Chiama commitWork]
    C --> D[Esegui operazioni DML con allOrNone=false]
    D --> E[Analizza i risultati]
    E --> F[Crea BulkDmlResult]
    F --> G[Cancella i record registrati]
    G --> H[Restituisci il risultato]

Componenti Principali

IBulkDmlService

L’interfaccia principale che definisce il contratto per il servizio DML di massa:

public interface IBulkDmlService {
    // Registra record per l'inserimento
    void registerNew(List<SObject> records);
    void registerNew(SObject record);
    
    // Registra record per l'aggiornamento
    void registerUpdate(List<SObject> records);
    void registerUpdate(SObject record);
    
    // Registra record per l'upsert
    void registerUpsert(List<SObject> records);
    void registerUpsert(SObject record);
    
    // Esegui tutte le operazioni registrate
    IBulkDmlResult commitWork();
    
    // Cancella i record senza eseguire
    void clear();
}

Caratteristiche del design:

  • Metodi sovraccaricati per accettare sia liste che singoli record.
  • Nomenclatura coerente con fflib Unit of Work.
  • Restituzione di risultati strutturati per analisi successive.

IBulkDmlResult

Interfaccia che incapsula i risultati di tutte le operazioni DML:

public interface IBulkDmlResult {
    Database.SaveResult[] getInsertResults();
    Database.SaveResult[] getUpdateResults();
    Database.UpsertResult[] getUpsertResults();
    
    Boolean isAllSuccess();
    Integer getSuccessCount();
    Integer getFailureCount();
    Integer getTotalCount();
}

Questa interfaccia consente l’analisi programmatica dei risultati e il processo decisionale basato sul successo o sul fallimento delle operazioni.

Funzionamento Interno: Come funziona il Framework

1. Archiviazione dei Record

Internamente, BulkDmlService mantiene tre collezioni private per archiviare i record registrati:

private List<SObject> recordsToInsert;
private List<SObject> recordsToUpdate;
private List<SObject> recordsToUpsert;

Quando chiami registerNew(), registerUpdate() o registerUpsert(), i record vengono aggiunti a queste rispettive collezioni. Il servizio convalida anche che i record non siano null prima di aggiungerli e registra ogni operazione nel logger.

2. Processo di Commit

Il metodo commitWork() è dove avviene la magia:

public IBulkDmlResult commitWork() {
    // 1. Logging iniziale
    Logger.info(new LogMessage('Commiting bulk DML work - Insert: {0}, Update: {1}, Upsert: {2}', 
        new List<Object>{recordsToInsert.size(), recordsToUpdate.size(), recordsToUpsert.size()}));
    
    // 2. Inizializza gli array di risultati
    Database.SaveResult[] insertResults = new List<Database.SaveResult>();
    Database.SaveResult[] updateResults = new List<Database.SaveResult>();
    Database.UpsertResult[] upsertResults = new List<Database.UpsertResult>();
    
    try {
        // 3. Esegui operazioni DML con allOrNone = false
        if (!recordsToInsert.isEmpty()) {
            insertResults = performInsert(recordsToInsert);
        }
        
        if (!recordsToUpdate.isEmpty()) {
            updateResults = performUpdate(recordsToUpdate);
        }
        
        if (!recordsToUpsert.isEmpty()) {
            upsertResults = performUpsert(recordsToUpsert);
        }
        
        // 4. Crea un risultato consolidato
        IBulkDmlResult result = new BulkDmlResult(insertResults, updateResults, upsertResults);
        
        // 5. Logging dei risultati
        Logger.info(new LogMessage('Bulk DML completed - Total: {0}, Success: {1}, Failures: {2}', 
            new List<Object>{result.getSuccessCount(), result.getFailureCount(), result.getTotalCount()}));
        
        return result;
        
    } catch (Exception ex) {
        // 6. Gestione delle eccezioni
        Logger.error(new LogMessage('Bulk DML operation failed: {0}', ex.getMessage()))
            .setExceptionDetails(ex);
        throw new BulkDmlException('Bulk DML operation failed: ' + ex.getMessage(), ex);
    } finally {
        // 7. Cancella sempre i record registrati dopo il tentativo di commit
        this.clear();
    }
}

Nota sul sistema di logging: Nell’implementazione di riferimento mostrata in questo articolo, viene utilizzato Nebula Logger (https://github.com/jongpie/NebulaLogger), una robusta soluzione di osservabilità per Salesforce. Tuttavia, il framework è completamente agnostico dal sistema di logging utilizzato. Puoi integrare qualsiasi sistema di logging preferito (Logger, System.debug, logging personalizzato, ecc.) adattando le chiamate di logging nell’implementazione alle tue esigenze.

3. Esecuzione dell’Operazione DML

Ogni operazione DML viene eseguita utilizzando Database.insert(), Database.update() o Database.upsert() con il parametro allOrNone = false:

@TestVisible
private Database.SaveResult[] performInsert(List<SObject> records) {
    Logger.info(new LogMessage('Executing insert for {0} records', records.size()));
    
    try {
        // La chiave è qui: allOrNone = false consente il successo parziale
        Database.SaveResult[] results = Database.insert(records, false);
        
        // Analizza e registra i risultati falliti
        analyzeAndLogSaveResults('INSERT', results, records);
        
        return results;
    } catch (DmlException dmlEx) {
        Logger.error(new LogMessage('Insert operation failed: {0}', dmlEx.getMessage()))
            .setExceptionDetails(dmlEx);
        throw new BulkDmlException('Insert operation failed: ' + dmlEx.getMessage(), dmlEx);
    }
}

Punto chiave: Il secondo parametro false in Database.insert(records, false) è ciò che abilita il successo parziale. Con true, l’intera operazione fallirebbe se un singolo record ha problemi.

4. Analisi dei Risultati

Il framework analizza automaticamente i risultati dopo ogni operazione DML:

@TestVisible
private void analyzeAndLogSaveResults(String operation, Database.SaveResult[] results, List<SObject> records) {
    Database.SaveResult[] failedResults = new List<Database.SaveResult>();
    
    // Raccogli tutti i risultati falliti
    for (Database.SaveResult result : results) {
        if (!result.isSuccess()) {
            failedResults.add(result);
        }
    }
    
    // Registra solo i fallimenti (i successi vengono registrati a livello INFO)
    if (!failedResults.isEmpty()) {
        Logger.error(new LogMessage('{0} operation completed with {1} failures out of {2} records', 
            new List<Object>{operation, failedResults.size(), results.size()}))
            .setDatabaseResult(failedResults)
            .setRecord(records);
    }
}

Nota: Il codice di esempio utilizza Nebula Logger, ma puoi adattare le chiamate di logging al tuo sistema preferito. Il framework non dipende da alcun sistema di logging specifico.

5. Costruzione del Risultato

BulkDmlResult consolida tutti i risultati in un unico oggetto:

public class BulkDmlResult implements IBulkDmlResult {
    private Database.SaveResult[] insertResults;
    private Database.SaveResult[] updateResults;
    private Database.UpsertResult[] upsertResults;
    
    public Integer getSuccessCount() {
        Integer successCount = 0;
        
        // Conta gli inserimenti riusciti
        for (Database.SaveResult result : insertResults) {
            if (result.isSuccess()) {
                successCount++;
            }
        }
        
        // Conta gli aggiornamenti riusciti
        for (Database.SaveResult result : updateResults) {
            if (result.isSuccess()) {
                successCount++;
            }
        }
        
        // Conta gli upsert riusciti
        for (Database.UpsertResult result : upsertResults) {
            if (result.isSuccess()) {
                successCount++;
            }
        }
        
        return successCount;
    }
    
    public Integer getFailureCount() {
        return getTotalCount() - getSuccessCount();
    }
    
    public Boolean isAllSuccess() {
        return getFailureCount() == 0 && getTotalCount() > 0;
    }
}

Esempi Pratici di Utilizzo

Esempio 1: Elaborazione di Dati di Massa

Immagina uno scenario in cui devi elaborare 1000 Lead ricevuti da un’integrazione esterna. Alcuni potrebbero avere dati non validi, ma vuoi salvare quelli validi:

public class LeadImportService {
    public void processLeads(List<Lead> leadsToProcess) {
        // Ottieni il servizio dalla Application factory (supporta il mocking)
        IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
        
        List<Lead> validLeads = new List<Lead>();
        List<Lead> invalidLeads = new List<Lead>();
        
        // Separa i lead validi da quelli non validi
        for (Lead lead : leadsToProcess) {
            if (isValidLead(lead)) {
                validLeads.add(lead);
            } else {
                invalidLeads.add(lead);
            }
        }
        
        // Registra solo i lead validi
        bulkDmlService.registerNew(validLeads);
        
        // Esegui le operazioni
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Analizza i risultati
        if (!result.isAllSuccess()) {
            Logger.warn(new LogMessage('Successo parziale nell'importazione dei lead. {0} riusciti, {1} falliti su {2} totali', 
                new List<Object>{result.getSuccessCount(), result.getFailureCount(), result.getTotalCount()}))
                .setDatabaseResult(result.getInsertResults());
            
            // Elabora errori specifici se necessario
            processFailedLeads(result.getInsertResults(), validLeads);
        }
        
        // Notifica i lead non validi che non sono nemmeno stati tentati di elaborare
        if (!invalidLeads.isEmpty()) {
            Logger.warn(new LogMessage('{0} lead sono stati rifiutati prima del DML a causa di errori di validazione', 
                invalidLeads.size()));
        }
    }
    
    private Boolean isValidLead(Lead lead) {
        // La tua logica di validazione qui
        return String.isNotBlank(lead.LastName) && String.isNotBlank(lead.Email);
    }
    
    private void processFailedLeads(Database.SaveResult[] results, List<Lead> leads) {
        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                Lead failedLead = leads[i];
                Database.Error[] errors = results[i].getErrors();
                
                // Elabora ogni errore
                for (Database.Error error : errors) {
                    Logger.error(new LogMessage('Lead {0} fallito: {1}', 
                        new List<Object>{failedLead.Email, error.getMessage()}))
                        .setRecord(failedLead);
                }
            }
        }
    }
}

Esempio 2: Aggiornamento di Massa con Validazioni

Aggiorna più Account ma consenti ad alcuni di fallire:

public class AccountUpdateService {
    public void updateAccountsBasedOnCriteria(List<Account> accounts) {
        IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
        
        List<Account> accountsToUpdate = new List<Account>();
        
        // Applica la logica di business per determinare cosa aggiornare
        for (Account acc : accounts) {
            if (acc.AnnualRevenue > 1000000) {
                acc.Type = 'Enterprise';
                accountsToUpdate.add(acc);
            }
        }
        
        // Registra per l'aggiornamento
        bulkDmlService.registerUpdate(accountsToUpdate);
        
        // Esegui
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Logging dei risultati
        Logger.info(new LogMessage('Aggiornati {0} su {1} account con successo', 
            new List<Object>{result.getSuccessCount(), result.getTotalCount()}));
    }
}

Esempio 3: Operazioni Miste (Insert, Update, Upsert)

Un caso più complesso in cui è necessario eseguire più tipi di operazioni:

public class ContactSyncService {
    public void syncContacts(List<Contact> contactsToSync) {
        IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
        
        List<Contact> newContacts = new List<Contact>();
        List<Contact> existingContacts = new List<Contact>();
        List<Contact> contactsToUpsert = new List<Contact>();
        
        // Classifica i contatti in base al loro stato
        for (Contact contact : contactsToSync) {
            if (contact.Id == null) {
                newContacts.add(contact);
            } else if (shouldUpdate(contact)) {
                existingContacts.add(contact);
            } else {
                contactsToUpsert.add(contact);
            }
        }
        
        // Registra tutte le operazioni
        bulkDmlService.registerNew(newContacts);
        bulkDmlService.registerUpdate(existingContacts);
        bulkDmlService.registerUpsert(contactsToUpsert);
        
        // Esegui tutte le operazioni in una singola transazione
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Analizza i risultati per tipo di operazione
        analyzeResults(result, newContacts, existingContacts, contactsToUpsert);
    }
    
    private Boolean shouldUpdate(Contact contact) {
        // Logica per determinare se deve essere aggiornato
        return contact.LastModifiedDate < DateTime.now().addHours(-24);
    }
    
    private void analyzeResults(IBulkDmlResult result, List<Contact> inserts, 
                                List<Contact> updates, List<Contact> upserts) {
        // Analizza gli inserimenti
        if (!result.getInsertResults().isEmpty()) {
            Logger.info(new LogMessage('Inserimenti: {0} riusciti, {1} falliti', 
                new List<Object>{countSuccesses(result.getInsertResults()), 
                               countFailures(result.getInsertResults())}));
        }
        
        // Analizza gli aggiornamenti
        if (!result.getUpdateResults().isEmpty()) {
            Logger.info(new LogMessage('Aggiornamenti: {0} riusciti, {1} falliti', 
                new List<Object>{countSuccesses(result.getUpdateResults()), 
                               countFailures(result.getUpdateResults())}));
        }
        
        // Analizza gli upsert
        if (!result.getUpsertResults().isEmpty()) {
            Logger.info(new LogMessage('Upsert: {0} riusciti, {1} falliti', 
                new List<Object>{countSuccesses(result.getUpsertResults()), 
                               countFailures(result.getUpsertResults())}));
        }
    }
    
    private Integer countSuccesses(Database.SaveResult[] results) {
        Integer count = 0;
        for (Database.SaveResult result : results) {
            if (result.isSuccess()) count++;
        }
        return count;
    }
    
    private Integer countFailures(Database.SaveResult[] results) {
        Integer count = 0;
        for (Database.SaveResult result : results) {
            if (!result.isSuccess()) count++;
        }
        return count;
    }
    
    private Integer countSuccesses(Database.UpsertResult[] results) {
        Integer count = 0;
        for (Database.UpsertResult result : results) {
            if (result.isSuccess()) count++;
        }
        return count;
    }
    
    private Integer countFailures(Database.UpsertResult[] results) {
        Integer count = 0;
        for (Database.UpsertResult result : results) {
            if (!result.isSuccess()) count++;
        }
        return count;
    }
}

Testing e Mocking: Test Unitari senza Database

Una delle ragioni principali per la creazione di questo framework è fornire un sistema di mocking semplice per le operazioni in massa utilizzando lo stesso pattern di dependency injection di fflib. Ciò consente di scrivere veri test unitari senza la necessità di toccare il database, seguendo le stesse pratiche che già conosci con fflib.

Il sistema di mocking è completamente integrato con la Application factory di fflib, permettendoti di utilizzare lo stesso pattern familiare per iniettare i mock nei tuoi test.

BulkDmlServiceMock

Il mock include capacità avanzate per simulare diversi scenari:

@isTest
private class LeadImportServiceTest {
    @isTest
    static void testProcessLeadsWithPartialSuccess() {
        // Dato
        List<Lead> testLeads = new List<Lead>{
            new Lead(LastName = 'Test1', Email = 'test1@example.com', Company = 'Company1'),
            new Lead(LastName = 'Test2', Email = 'test2@example.com', Company = 'Company2'),
            new Lead(LastName = 'Test3', Email = 'test3@example.com', Company = 'Company3')
        };
        
        // Configura il mock per il successo parziale
        BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock()
            .withPartialSuccess(2); // 2 successi, 1 fallimento
        
        Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
        
        LeadImportService service = new LeadImportService();
        
        // Quando
        Test.startTest();
        service.processLeads(testLeads);
        Test.stopTest();
        
        // Allora
        // Verifica che i lead siano stati registrati
        System.assertEquals(3, mockBulkDmlService.registeredInserts.size(), 
            'Tutti i lead dovrebbero essere registrati');
        
        // Verifica che commitWork sia stato chiamato
        System.assertEquals(1, mockBulkDmlService.commitWorkCallCount, 
            'commitWork dovrebbe essere chiamato una volta');
        
        // Verifica i risultati
        IBulkDmlResult result = mockBulkDmlService.commitResults[0];
        System.assertEquals(2, result.getSuccessCount(), 
            'Due lead dovrebbero avere successo');
        System.assertEquals(1, result.getFailureCount(), 
            'Un lead dovrebbe fallire');
        System.assertFalse(result.isAllSuccess(), 
            'Non tutte le operazioni dovrebbero avere successo');
    }
}

Simulazione di Errori Personalizzati

Il mock ti consente di simulare errori specifici:

@isTest
private class LeadImportServiceErrorTest {
    @isTest
    static void testProcessLeadsWithCustomErrors() {
        // Dato
        List<Lead> testLeads = new List<Lead>{
            new Lead(LastName = 'Test1', Email = 'test1@example.com', Company = 'Company1'),
            new Lead(LastName = 'Test2', Email = 'test2@example.com', Company = 'Company2')
        };
        
        // Configura il mock con errori personalizzati
        BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock()
            .withPartialSuccess(1) // 1 successo, 1 fallimento
            .withErrorMessages(new List<String>{'Validazione personalizzata fallita'})
            .withErrorCodes(new List<System.StatusCode>{System.StatusCode.FIELD_CUSTOM_VALIDATION_EXCEPTION});
        
        Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
        
        LeadImportService service = new LeadImportService();
        
        // Quando
        Test.startTest();
        service.processLeads(testLeads);
        Test.stopTest();
        
        // Allora
        IBulkDmlResult result = mockBulkDmlService.commitResults[0];
        Database.SaveResult[] insertResults = result.getInsertResults();
        
        // Verifica che il secondo inserimento sia fallito con l'errore personalizzato
        System.assertFalse(insertResults[1].isSuccess(), 
            'Il secondo inserimento sarebbe dovuto fallire');
        System.assertEquals('Validazione personalizzata fallita', 
            insertResults[1].getErrors()[0].getMessage(), 
            'Il messaggio di errore dovrebbe corrispondere');
        System.assertEquals(System.StatusCode.FIELD_CUSTOM_VALIDATION_EXCEPTION, 
            insertResults[1].getErrors()[0].getStatusCode(), 
            'Il codice di errore dovrebbe corrispondere');
    }
}

Verifica delle Interazioni

Il mock consente anche di verificare che i metodi corretti siano stati chiamati:

@isTest
private class ContactSyncServiceTest {
    @isTest
    static void testSyncContactsWithMixedOperations() {
        // Dato
        Id accountId = fflib_IDGenerator.generate(Account.SObjectType);
        List<Contact> contacts = new List<Contact>{
            new Contact(LastName = 'Nuovo', AccountId = accountId), // Nuovo
            new Contact(Id = fflib_IDGenerator.generate(Contact.SObjectType), 
                       LastName = 'Esistente', AccountId = accountId), // Esistente
            new Contact(LastName = 'Upsert', AccountId = accountId) // Upsert
        };
        
        BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock().withAllSuccess();
        Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
        
        ContactSyncService service = new ContactSyncService();
        
        // Quando
        Test.startTest();
        service.syncContacts(contacts);
        Test.stopTest();
        
        // Allora - Verifica che tutti i metodi di registrazione siano stati chiamati
        mockBulkDmlService.verifyCalls(1, 1, 1, 1); // 1 registerNew, 1 registerUpdate, 1 registerUpsert, 1 commitWork
    }
}

Casi d’Uso Avanzati

Caso d’Uso 1: Elaborazione Batch con Resilienza

In una classe batch, puoi utilizzare il Bulk DML Service per elaborare grandi volumi di dati con resilienza:

public class DataMigrationBatch implements Database.Batchable<SObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE Processed__c = false');
    }
    
    public void execute(Database.BatchableContext bc, List<Account> accounts) {
        IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
        
        List<Account> accountsToUpdate = new List<Account>();
        
        // Elabora ogni account
        for (Account acc : accounts) {
            // Applica le trasformazioni
            acc.Processed__c = true;
            acc.ProcessedDate__c = DateTime.now();
            accountsToUpdate.add(acc);
        }
        
        // Registra ed esegui
        bulkDmlService.registerUpdate(accountsToUpdate);
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Logging per il monitoraggio
        if (!result.isAllSuccess()) {
            Logger.warn(new LogMessage('Batch elaborato {0} su {1} account con successo', 
                new List<Object>{result.getSuccessCount(), result.getTotalCount()}))
                .setDatabaseResult(result.getUpdateResults());
        }
    }
    
    public void finish(Database.BatchableContext bc) {
        // Logica di finalizzazione
    }
}

Caso d’Uso 2: Integrazione con la Logica di Retry

Combina il Bulk DML Service con la logica di retry:

public class ResilientDataProcessor {
    private static final Integer MAX_RETRIES = 3;
    
    public void processWithRetry(List<SObject> records) {
        IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
        
        List<SObject> recordsToProcess = records;
        Integer attempt = 0;
        
        while (attempt < MAX_RETRIES && !recordsToProcess.isEmpty()) {
            attempt++;
            
            bulkDmlService.registerNew(recordsToProcess);
            IBulkDmlResult result = bulkDmlService.commitWork();
            
            if (result.isAllSuccess()) {
                Logger.info(new LogMessage('Tutti i record elaborati con successo al tentativo {0}', attempt));
                break;
            }
            
            // Identifica i record falliti per il retry
            recordsToProcess = identifyFailedRecords(result.getInsertResults(), recordsToProcess);
            
            if (!recordsToProcess.isEmpty()) {
                Logger.warn(new LogMessage('Il tentativo {0} è fallito. Riprovo {1} record', 
                    new List<Object>{attempt, recordsToProcess.size()}));
            }
        }
        
        if (!recordsToProcess.isEmpty()) {
            Logger.error(new LogMessage('Impossibile elaborare {0} record dopo {1} tentativi', 
                new List<Object>{recordsToProcess.size(), MAX_RETRIES}));
        }
    }
    
    private List<SObject> identifyFailedRecords(Database.SaveResult[] results, List<SObject> records) {
        List<SObject> failedRecords = new List<SObject>();
        
        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                failedRecords.add(records[i]);
            }
        }
        
        return failedRecords;
    }
}

Confronto con fflib Unit of Work

Caratteristicafflib Unit of WorkBulk DML Service
APIregisterNew(), registerDirty(), registerUpsert()registerNew(), registerUpdate(), registerUpsert()
AtomicitaTotale (allOrNone = true)Parziale (allOrNone = false)
RisultatiEccezioni in caso di fallimentoOggetto risultato dettagliato
Casi d’usoTransazioni criticheElaborazione di massa resiliente
LoggingBaseCompleto e strutturato (usando il tuo sistema preferito)
TestingRichiede databaseMock completo disponibile integrato con fflib
Dependency InjectionApplication factory di fflibApplication factory di fflib (stesso pattern)

Quando usare ciascuno?

Usa fflib Unit of Work quando:

  • Hai bisogno di una garanzia transazionale completa.
  • Le operazioni sono critiche per il business.
  • Un singolo fallimento deve annullare l’intera operazione.
  • Lavori con relazioni complesse che richiedono integrità.

Usa Bulk DML Service quando:

  • Elabori grandi volumi di dati.
  • Puoi tollerare alcuni fallimenti individuali.
  • Hai bisogno di resilienza nelle integrazioni.
  • Esegui migrazioni in cui il progresso parziale è prezioso.
  • Hai bisogno di un sistema di mocking semplice per operazioni di massa che si integra con fflib.

Complementarietà dei Framework

È importante capire che il Bulk DML Service Pattern non sostituisce fflib Unit of Work, ma lo completa. Entrambi i framework possono coesistere nello stesso progetto:

  • Unit of Work per operazioni transazionali critiche in cui hai bisogno di una garanzia completa.
  • Bulk DML Service per operazioni di massa in cui il successo parziale è accettabile e hai bisogno di un sistema di mocking integrato con fflib.

La chiave è utilizzare il framework giusto per ogni caso d’uso specifico.

Migliori Pratiche e Considerazioni

1. Validazione Pre-DML

Valida sempre i dati prima di registrarli:

public void processRecords(List<Account> accounts) {
    IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
    
    List<Account> validAccounts = new List<Account>();
    
    // Valida prima di registrare
    for (Account acc : accounts) {
        if (isValid(acc)) {
            validAccounts.add(acc);
        } else {
            Logger.warn(new LogMessage('L'account {0} ha fallito la validazione pre-DML', acc.Name));
        }
    }
    
    bulkDmlService.registerNew(validAccounts);
    IBulkDmlResult result = bulkDmlService.commitWork();
    // ...
}

2. Analisi dei Risultati

Analizza sempre i risultati dopo commitWork():

IBulkDmlResult result = bulkDmlService.commitWork();

if (!result.isAllSuccess()) {
    // Analizza i fallimenti specifici
    processFailures(result);
    
    // Logging appropriato
    Logger.warn(new LogMessage('Successo parziale: {0}/{1} operazioni riuscite', 
        new List<Object>{result.getSuccessCount(), result.getTotalCount()}));
}

3. Gestione degli Errori

Utilizza il logging strutturato per tracciare i problemi:

IBulkDmlResult result = bulkDmlService.commitWork();

if (!result.isAllSuccess()) {
    Database.SaveResult[] failedInserts = result.getInsertResults();
    
    for (Integer i = 0; i < failedInserts.size(); i++) {
        if (!failedInserts[i].isSuccess()) {
            Logger.error(new LogMessage('Inserimento {0} fallito: {1}', 
                new List<Object>{i, failedInserts[i].getErrors()[0].getMessage()}))
                .setDatabaseResult(new List<Database.SaveResult>{failedInserts[i]})
                .setRecord(records[i]);
        }
    }
}

4. Limiti del Governatore

Tieni presente che anche con allOrNone = false, sei comunque soggetto ai limiti del governatore di Salesforce:

  • Massimo 10.000 record per operazione DML.
  • Limiti di CPU e memoria.
  • Limiti delle query SOQL se utilizzate in precedenza.

5. Testing Completo

Scrivi test che coprano tutti gli scenari:

@isTest
private class MyServiceTest {
    @isTest
    static void testSuccessScenario() {
        // Test con successo totale.
    }
    
    @isTest
    static void testPartialSuccessScenario() {
        // Test con successo parziale.
    }
    
    @isTest
    static void testFailureScenario() {
        // Test con fallimenti totali.
    }
    
    @isTest
    static void testMixedOperationsScenario() {
        // Test con operazioni miste.
    }
}

Conclusione

Il Bulk DML Service Pattern è un framework potente che completa perfettamente l’fflib Unit of Work, fornendo una soluzione per scenari in cui sono necessarie operazioni DML resilienti con successo parziale.

Vantaggi Chiave

  • API familiare: Simile a Unit of Work, facile da adottare.
  • Resilienza: Consente di elaborare grandi volumi con tolleranza ai guasti.
  • Osservabilità: Logging completo e risultati dettagliati.
  • Testabile: Mock completo per veri test unitari.
  • Scalabile: Gestisce grandi volumi di dati in modo efficiente.

Quando Usarlo

Questo framework è particolarmente utile in:

  • Elaborazione batch di grandi volumi.
  • Migrazioni di dati.
  • Integrazioni con sistemi esterni.
  • Elaborazione di dati importati.
  • Qualsiasi scenario in cui il progresso parziale è prezioso.

Prossimi Passi

Per implementare questo pattern nella tua organizzazione:

  1. Assicurati che fflib sia installato: Questo è un prerequisito, poiché il framework utilizza la Application factory di fflib per la dependency injection.
  2. Implementa le interfacce e le classi del framework.
  3. Integrati con la tua Application factory di fflib per la dependency injection (lo stesso pattern che già conosci).
  4. Configura il tuo sistema di logging preferito (puoi usare Nebula Logger come riferimento: https://github.com/jongpie/NebulaLogger, o qualsiasi altro sistema di logging).
  5. Scrivi test utilizzando il mock fornito che si integra perfettamente con fflib.
  6. Inizia con casi d’uso semplici e scala gradualmente.

Il Bulk DML Service Pattern ti consente di costruire sistemi più resilienti e gestibili, specialmente quando lavori con grandi volumi di dati in cui la perfezione totale non è realistica ma il progresso parziale è prezioso. Inoltre, fornisce un sistema di mocking semplice per le operazioni di massa che si integra perfettamente con l’ecosistema fflib che già utilizzi nel tuo progetto.