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.
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
| Caratteristica | fflib Unit of Work | Bulk DML Service |
|---|---|---|
| API | registerNew(), registerDirty(), registerUpsert() | registerNew(), registerUpdate(), registerUpsert() |
| Atomicita | Totale (allOrNone = true) | Parziale (allOrNone = false) |
| Risultati | Eccezioni in caso di fallimento | Oggetto risultato dettagliato |
| Casi d’uso | Transazioni critiche | Elaborazione di massa resiliente |
| Logging | Base | Completo e strutturato (usando il tuo sistema preferito) |
| Testing | Richiede database | Mock completo disponibile integrato con fflib |
| Dependency Injection | Application factory di fflib | Application 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:
- Assicurati che fflib sia installato: Questo è un prerequisito, poiché il framework utilizza la Application factory di fflib per la dependency injection.
- Implementa le interfacce e le classi del framework.
- Integrati con la tua Application factory di fflib per la dependency injection (lo stesso pattern che già conosci).
- Configura il tuo sistema di logging preferito (puoi usare Nebula Logger come riferimento: https://github.com/jongpie/NebulaLogger, o qualsiasi altro sistema di logging).
- Scrivi test utilizzando il mock fornito che si integra perfettamente con fflib.
- 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.
Articoli Correlati
Salesforce Architettura Salesforce con FFLib: Pattern Enterprise
Guida completa all'implementazione dei pattern architetturali enterprise in Salesforce usando FFLib. Include Domain Layer, Selector Layer, Service Layer e Unit of Work.
Salesforce In evidenza Query Plan Architecture: Pattern per i Trigger Apex
Scopri come strutturare trigger complessi in Salesforce con il pattern Query Plan Architecture. Separa la logica in 4 fasi (Collect, Load, Run, Commit) per eliminare SOQL duplicati, rispettare i governor limit e semplificare il testing.
Salesforce In evidenza CI/CD per Salesforce: Automazione dei Deploy
Implementazione completa di pipeline CI/CD per Salesforce usando GitHub Actions, SFDX CLI e Scratch Orgs. Include best practice e troubleshooting.