Empieza a escribir para buscar.

Salesforce Salesforce FFLib DML

Bulk DML Service Pattern: Operaciones DML Parciales en Salesforce

Documentación técnica completa del framework Bulk DML Service Pattern para Salesforce. Aprende a realizar operaciones DML resilientes con éxito parcial usando una API familiar estilo Unit of Work.

25 min de lectura

Introducción: El Problema del Unit of Work y las Operaciones Parciales

Cuando trabajamos con fflib y su patrón Unit of Work, nos encontramos con una limitación importante: el fflib_SObjectUnitOfWork utiliza DML con allOrNone = true por defecto. Esto significa que si un solo registro falla la validación en un batch de 200 registros, toda la transacción se revierte y ningún registro se guarda.

Este comportamiento es excelente para garantizar integridad transaccional, pero hay escenarios donde necesitamos un enfoque diferente:

  • Procesamiento masivo de datos: Cuando procesamos grandes volúmenes de datos y queremos guardar los registros válidos aunque algunos fallen
  • Migraciones de datos: En procesos de migración donde algunos registros pueden fallar pero no queremos perder todo el progreso
  • Integraciones: Cuando recibimos datos externos y algunos pueden ser inválidos pero queremos procesar los válidos
  • Procesamiento batch: En clases batch donde queremos continuar aunque algunos registros fallen

fflib no proporciona una solución nativa para esta casuística, ya que el Unit of Work está diseñado para operaciones transaccionales completas donde todo o nada debe ejecutarse. Además, una de las razones principales para crear este framework es poder generar un sistema de mocking sencillo para operaciones bulk utilizando el mismo patrón de dependency injection que fflib, permitiendo escribir pruebas unitarias verdaderas sin necesidad de tocar la base de datos.

El Bulk DML Service Pattern viene a resolver estos problemas proporcionando una API familiar estilo Unit of Work pero con soporte para operaciones parciales usando allOrNone = false, y un sistema de mocking completo que se integra perfectamente con el Application factory de fflib.

¿Qué es el Bulk DML Service Pattern?

El Bulk DML Service Pattern es un framework diseñado para manejar operaciones DML (insert, update, upsert) en Salesforce con éxito parcial (allOrNone = false). Proporciona una API similar a fflib_SObjectUnitOfWork pero con la capacidad de procesar registros de forma que los fallos individuales no bloqueen toda la operación.

Requisitos Previos

⚠️ IMPORTANTE: Este framework requiere que tu proyecto utilice fflib como base arquitectónica. El Bulk DML Service Pattern está diseñado para integrarse con el Application factory de fflib, utilizando el mismo patrón de dependency injection para facilitar el mocking y las pruebas unitarias.

Características Clave

  • API familiar: Métodos similares a Unit of Work (registerNew, registerUpdate, registerUpsert, commitWork)
  • Operaciones parciales: Permite que algunos registros fallen sin afectar los demás
  • Resultados detallados: Proporciona un resumen completo de todas las operaciones realizadas
  • Logging integrado: Registra automáticamente los fallos y éxitos (utiliza tu sistema de logging preferido)
  • Testeable: Incluye un mock completo para pruebas unitarias sin base de datos que se integra con fflib
  • Manejo de errores: Excepciones personalizadas y logging estructurado
  • Integración con fflib: Utiliza el Application factory de fflib para dependency injection y mocking

Arquitectura del Framework

El framework está construido sobre una arquitectura basada en interfaces que permite mockear fácilmente para testing:

IBulkDmlService (Interface)

    ├── BulkDmlService (Implementación real)
    └── BulkDmlServiceMock (Mock para testing)

IBulkDmlResult (Interface)

    └── BulkDmlResult (Implementación)

Flujo de Operación

graph TD
    A[Crear instancia de BulkDmlService] --> B[Registrar registros con registerNew/Update/Upsert]
    B --> C[Llamar commitWork]
    C --> D[Ejecutar operaciones DML con allOrNone=false]
    D --> E[Analizar resultados]
    E --> F[Crear BulkDmlResult]
    F --> G[Limpiar registros registrados]
    G --> H[Retornar resultado]

Componentes Principales

IBulkDmlService

La interfaz principal que define el contrato para el servicio de DML masivo:

public interface IBulkDmlService {
    // Registro de registros para insert
    void registerNew(List<SObject> records);
    void registerNew(SObject record);
    
    // Registro de registros para update
    void registerUpdate(List<SObject> records);
    void registerUpdate(SObject record);
    
    // Registro de registros para upsert
    void registerUpsert(List<SObject> records);
    void registerUpsert(SObject record);
    
    // Ejecutar todas las operaciones registradas
    IBulkDmlResult commitWork();
    
    // Limpiar registros sin ejecutar
    void clear();
}

Características del diseño:

  • Métodos sobrecargados para aceptar tanto listas como registros individuales
  • Nomenclatura consistente con fflib Unit of Work
  • Retorno de resultados estructurados para análisis posterior

IBulkDmlResult

Interfaz que encapsula los resultados de todas las operaciones DML:

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

Esta interfaz permite analizar los resultados de forma programática y tomar decisiones basadas en el éxito o fracaso de las operaciones.

Funcionamiento Interno: Cómo Funciona el Framework

1. Almacenamiento de Registros

Internamente, BulkDmlService mantiene tres colecciones privadas para almacenar los registros registrados:

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

Cuando llamas a registerNew(), registerUpdate() o registerUpsert(), los registros se añaden a estas colecciones respectivas. El servicio también valida que los registros no sean null antes de añadirlos y registra cada operación en el logger.

2. Proceso de Commit

El método commitWork() es donde ocurre la magia:

public IBulkDmlResult commitWork() {
    // 1. Logging inicial
    Logger.info(new LogMessage('Committing bulk DML work - Insert: {0}, Update: {1}, Upsert: {2}', 
        new List<Object>{recordsToInsert.size(), recordsToUpdate.size(), recordsToUpsert.size()}));
    
    // 2. Inicializar arrays de resultados
    Database.SaveResult[] insertResults = new List<Database.SaveResult>();
    Database.SaveResult[] updateResults = new List<Database.SaveResult>();
    Database.UpsertResult[] upsertResults = new List<Database.UpsertResult>();
    
    try {
        // 3. Ejecutar operaciones DML con allOrNone = false
        if (!recordsToInsert.isEmpty()) {
            insertResults = performInsert(recordsToInsert);
        }
        
        if (!recordsToUpdate.isEmpty()) {
            updateResults = performUpdate(recordsToUpdate);
        }
        
        if (!recordsToUpsert.isEmpty()) {
            upsertResults = performUpsert(recordsToUpsert);
        }
        
        // 4. Crear resultado consolidado
        IBulkDmlResult result = new BulkDmlResult(insertResults, updateResults, upsertResults);
        
        // 5. Logging de resultados
        Logger.info(new LogMessage('Bulk DML completed - Total: {0}, Success: {1}, Failures: {2}', 
            new List<Object>{result.getTotalCount(), result.getSuccessCount(), result.getFailureCount()}));
        
        return result;
        
    } catch (Exception ex) {
        // 6. Manejo de excepciones
        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. Limpiar siempre los registros registrados
        this.clear();
    }
}

Nota sobre el sistema de logging: En la implementación de referencia que se muestra en este artículo, se utiliza Nebula Logger (https://github.com/jongpie/NebulaLogger), una solución robusta de observabilidad para Salesforce. Sin embargo, el framework es completamente agnóstico del sistema de logging utilizado. Puedes integrar cualquier sistema de logging que prefieras (Logger, System.debug, custom logging, etc.) adaptando las llamadas de logging en la implementación según tus necesidades.

3. Ejecución de Operaciones DML

Cada operación DML se ejecuta usando Database.insert(), Database.update() o Database.upsert() con el parámetro allOrNone = false:

@TestVisible
private Database.SaveResult[] performInsert(List<SObject> records) {
    Logger.info(new LogMessage('Executing insert for {0} records', records.size()));
    
    try {
        // La clave está aquí: allOrNone = false permite éxito parcial
        Database.SaveResult[] results = Database.insert(records, false);
        
        // Analizar y registrar resultados fallidos
        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 clave: El segundo parámetro false en Database.insert(records, false) es lo que permite el éxito parcial. Con true, toda la operación fallaría si un solo registro tiene problemas.

4. Análisis de Resultados

El framework analiza automáticamente los resultados después de cada operación DML:

@TestVisible
private void analyzeAndLogSaveResults(String operation, Database.SaveResult[] results, List<SObject> records) {
    Database.SaveResult[] failedResults = new List<Database.SaveResult>();
    
    // Recopilar todos los resultados fallidos
    for (Database.SaveResult result : results) {
        if (!result.isSuccess()) {
            failedResults.add(result);
        }
    }
    
    // Registrar solo los fallos (los éxitos se registran a nivel 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: El código de ejemplo utiliza Nebula Logger, pero puedes adaptar las llamadas de logging a tu sistema preferido. El framework no depende de ningún sistema de logging específico.

5. Construcción del Resultado

BulkDmlResult consolida todos los resultados en un objeto único:

public class BulkDmlResult implements IBulkDmlResult {
    private Database.SaveResult[] insertResults;
    private Database.SaveResult[] updateResults;
    private Database.UpsertResult[] upsertResults;
    
    public Integer getSuccessCount() {
        Integer successCount = 0;
        
        // Contar inserts exitosos
        for (Database.SaveResult result : insertResults) {
            if (result.isSuccess()) {
                successCount++;
            }
        }
        
        // Contar updates exitosos
        for (Database.SaveResult result : updateResults) {
            if (result.isSuccess()) {
                successCount++;
            }
        }
        
        // Contar upserts exitosos
        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;
    }
}

Ejemplos Prácticos de Uso

Ejemplo 1: Procesamiento de Datos Masivos

Imagina un escenario donde necesitas procesar 1000 Leads recibidos de una integración externa. Algunos pueden tener datos inválidos, pero quieres guardar los válidos:

public class LeadImportService {
    public void processLeads(List<Lead> leadsToProcess) {
        // Obtener el servicio desde Application factory (soporta mocking)
        IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
        
        List<Lead> validLeads = new List<Lead>();
        List<Lead> invalidLeads = new List<Lead>();
        
        // Separar leads válidos de inválidos
        for (Lead lead : leadsToProcess) {
            if (isValidLead(lead)) {
                validLeads.add(lead);
            } else {
                invalidLeads.add(lead);
            }
        }
        
        // Registrar solo los leads válidos
        bulkDmlService.registerNew(validLeads);
        
        // Ejecutar las operaciones
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Analizar resultados
        if (!result.isAllSuccess()) {
            Logger.warn(new LogMessage('Partial success importing leads. {0} succeeded, {1} failed out of {2} total', 
                new List<Object>{result.getSuccessCount(), result.getFailureCount(), result.getTotalCount()}))
                .setDatabaseResult(result.getInsertResults());
            
            // Procesar errores específicos si es necesario
            processFailedLeads(result.getInsertResults(), validLeads);
        }
        
        // Notificar sobre leads inválidos que ni siquiera se intentaron procesar
        if (!invalidLeads.isEmpty()) {
            Logger.warn(new LogMessage('{0} leads were rejected before DML due to validation errors', 
                invalidLeads.size()));
        }
    }
    
    private Boolean isValidLead(Lead lead) {
        // Tu lógica de validación aquí
        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();
                
                // Procesar cada error
                for (Database.Error error : errors) {
                    Logger.error(new LogMessage('Lead {0} failed: {1}', 
                        new List<Object>{failedLead.Email, error.getMessage()}))
                        .setRecord(failedLead);
                }
            }
        }
    }
}

Ejemplo 2: Actualización Masiva con Validaciones

Actualizar múltiples Accounts pero permitir que algunos fallen:

public class AccountUpdateService {
    public void updateAccountsBasedOnCriteria(List<Account> accounts) {
        IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
        
        List<Account> accountsToUpdate = new List<Account>();
        
        // Aplicar lógica de negocio para determinar qué actualizar
        for (Account acc : accounts) {
            if (acc.AnnualRevenue > 1000000) {
                acc.Type = 'Enterprise';
                accountsToUpdate.add(acc);
            }
        }
        
        // Registrar para actualización
        bulkDmlService.registerUpdate(accountsToUpdate);
        
        // Ejecutar
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Logging de resultados
        Logger.info(new LogMessage('Updated {0} out of {1} accounts successfully', 
            new List<Object>{result.getSuccessCount(), result.getTotalCount()}));
    }
}

Ejemplo 3: Operaciones Mixtas (Insert, Update, Upsert)

Un caso más complejo donde necesitas realizar múltiples tipos de operaciones:

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>();
        
        // Clasificar contactos según su estado
        for (Contact contact : contactsToSync) {
            if (contact.Id == null) {
                newContacts.add(contact);
            } else if (shouldUpdate(contact)) {
                existingContacts.add(contact);
            } else {
                contactsToUpsert.add(contact);
            }
        }
        
        // Registrar todas las operaciones
        bulkDmlService.registerNew(newContacts);
        bulkDmlService.registerUpdate(existingContacts);
        bulkDmlService.registerUpsert(contactsToUpsert);
        
        // Ejecutar todas las operaciones en una sola transacción
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Analizar resultados por tipo de operación
        analyzeResults(result, newContacts, existingContacts, contactsToUpsert);
    }
    
    private Boolean shouldUpdate(Contact contact) {
        // Lógica para determinar si debe actualizarse
        return contact.LastModifiedDate < DateTime.now().addHours(-24);
    }
    
    private void analyzeResults(IBulkDmlResult result, List<Contact> inserts, 
                                List<Contact> updates, List<Contact> upserts) {
        // Analizar inserts
        if (!result.getInsertResults().isEmpty()) {
            Logger.info(new LogMessage('Inserts: {0} succeeded, {1} failed', 
                new List<Object>{countSuccesses(result.getInsertResults()), 
                               countFailures(result.getInsertResults())}));
        }
        
        // Analizar updates
        if (!result.getUpdateResults().isEmpty()) {
            Logger.info(new LogMessage('Updates: {0} succeeded, {1} failed', 
                new List<Object>{countSuccesses(result.getUpdateResults()), 
                               countFailures(result.getUpdateResults())}));
        }
        
        // Analizar upserts
        if (!result.getUpsertResults().isEmpty()) {
            Logger.info(new LogMessage('Upserts: {0} succeeded, {1} failed', 
                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 y Mocking: Pruebas Unitarias sin Base de Datos

Una de las razones principales para crear este framework es poder generar un sistema de mocking sencillo para operaciones bulk utilizando el mismo patrón de dependency injection que fflib. Esto permite escribir pruebas unitarias verdaderas sin necesidad de tocar la base de datos, siguiendo las mismas prácticas que ya conoces con fflib.

El sistema de mocking está completamente integrado con el Application factory de fflib, permitiendo que uses el mismo patrón familiar para inyectar mocks en tus tests.

BulkDmlServiceMock

El mock incluye capacidades avanzadas para simular diferentes escenarios:

@isTest
private class LeadImportServiceTest {
    @isTest
    static void testProcessLeadsWithPartialSuccess() {
        // Given
        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')
        };
        
        // Configurar mock para éxito parcial
        BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock()
            .withPartialSuccess(2); // 2 éxitos, 1 fallo
        
        Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
        
        LeadImportService service = new LeadImportService();
        
        // When
        Test.startTest();
        service.processLeads(testLeads);
        Test.stopTest();
        
        // Then
        // Verificar que se registraron los leads
        System.assertEquals(3, mockBulkDmlService.registeredInserts.size(), 
            'All leads should be registered');
        
        // Verificar que se llamó commitWork
        System.assertEquals(1, mockBulkDmlService.commitWorkCallCount, 
            'commitWork should be called once');
        
        // Verificar resultados
        IBulkDmlResult result = mockBulkDmlService.commitResults[0];
        System.assertEquals(2, result.getSuccessCount(), 
            'Two leads should succeed');
        System.assertEquals(1, result.getFailureCount(), 
            'One lead should fail');
        System.assertFalse(result.isAllSuccess(), 
            'Not all operations should succeed');
    }
}

Simulación de Errores Personalizados

El mock permite simular errores específicos:

@isTest
private class LeadImportServiceErrorTest {
    @isTest
    static void testProcessLeadsWithCustomErrors() {
        // Given
        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')
        };
        
        // Configurar mock con errores personalizados
        BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock()
            .withPartialSuccess(1) // 1 éxito, 1 fallo
            .withErrorMessages(new List<String>{'Custom validation failed'})
            .withErrorCodes(new List<System.StatusCode>{System.StatusCode.FIELD_CUSTOM_VALIDATION_EXCEPTION});
        
        Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
        
        LeadImportService service = new LeadImportService();
        
        // When
        Test.startTest();
        service.processLeads(testLeads);
        Test.stopTest();
        
        // Then
        IBulkDmlResult result = mockBulkDmlService.commitResults[0];
        Database.SaveResult[] insertResults = result.getInsertResults();
        
        // Verificar que el segundo insert falló con el error personalizado
        System.assertFalse(insertResults[1].isSuccess(), 
            'Second insert should have failed');
        System.assertEquals('Custom validation failed', 
            insertResults[1].getErrors()[0].getMessage(), 
            'Error message should match');
        System.assertEquals(System.StatusCode.FIELD_CUSTOM_VALIDATION_EXCEPTION, 
            insertResults[1].getErrors()[0].getStatusCode(), 
            'Error code should match');
    }
}

Verificación de Interacciones

El mock también permite verificar que se llamaron los métodos correctos:

@isTest
private class ContactSyncServiceTest {
    @isTest
    static void testSyncContactsWithMixedOperations() {
        // Given
        Id accountId = fflib_IDGenerator.generate(Account.SObjectType);
        List<Contact> contacts = new List<Contact>{
            new Contact(LastName = 'New', AccountId = accountId), // Nuevo
            new Contact(Id = fflib_IDGenerator.generate(Contact.SObjectType), 
                       LastName = 'Existing', AccountId = accountId), // Existente
            new Contact(LastName = 'Upsert', AccountId = accountId) // Upsert
        };
        
        BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock().withAllSuccess();
        Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
        
        ContactSyncService service = new ContactSyncService();
        
        // When
        Test.startTest();
        service.syncContacts(contacts);
        Test.stopTest();
        
        // Then - Verificar que se llamaron todos los métodos de registro
        mockBulkDmlService.verifyCalls(1, 1, 1, 1); // 1 registerNew, 1 registerUpdate, 1 registerUpsert, 1 commitWork
    }
}

Casos de Uso Avanzados

Caso de Uso 1: Procesamiento Batch con Resiliencia

En una clase batch, puedes usar el Bulk DML Service para procesar grandes volúmenes de datos con resiliencia:

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>();
        
        // Procesar cada cuenta
        for (Account acc : accounts) {
            // Aplicar transformaciones
            acc.Processed__c = true;
            acc.ProcessedDate__c = DateTime.now();
            accountsToUpdate.add(acc);
        }
        
        // Registrar y ejecutar
        bulkDmlService.registerUpdate(accountsToUpdate);
        IBulkDmlResult result = bulkDmlService.commitWork();
        
        // Logging para monitoreo
        if (!result.isAllSuccess()) {
            Logger.warn(new LogMessage('Batch processed {0} out of {1} accounts successfully', 
                new List<Object>{result.getSuccessCount(), result.getTotalCount()}))
                .setDatabaseResult(result.getUpdateResults());
        }
    }
    
    public void finish(Database.BatchableContext bc) {
        // Lógica de finalización
    }
}

Caso de Uso 2: Integración con Retry Logic

Combinar el Bulk DML Service con lógica de reintento:

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('All records processed successfully on attempt {0}', attempt));
                break;
            }
            
            // Identificar registros fallidos para reintento
            recordsToProcess = identifyFailedRecords(result.getInsertResults(), recordsToProcess);
            
            if (!recordsToProcess.isEmpty()) {
                Logger.warn(new LogMessage('Attempt {0} failed. Retrying {1} records', 
                    new List<Object>{attempt, recordsToProcess.size()}));
            }
        }
        
        if (!recordsToProcess.isEmpty()) {
            Logger.error(new LogMessage('Failed to process {0} records after {1} attempts', 
                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;
    }
}

Comparación con fflib Unit of Work

Característicafflib Unit of WorkBulk DML Service
APIregisterNew(), registerDirty(), registerUpsert()registerNew(), registerUpdate(), registerUpsert()
AtomicidadTotal (allOrNone = true)Parcial (allOrNone = false)
ResultadosExcepciones en falloObjeto de resultados detallado
Casos de usoTransacciones críticasProcesamiento masivo resiliente

¿Cuándo usar cada uno?

Usa fflib Unit of Work cuando:

  • Necesitas garantía transaccional total
  • Las operaciones son críticas para el negocio
  • Un fallo debe revertir toda la operación
  • Trabajas con relaciones complejas que requieren integridad

Usa Bulk DML Service cuando:

  • Procesas grandes volúmenes de datos
  • Puedes tolerar algunos fallos individuales
  • Necesitas resiliencia en integraciones
  • Realizas migraciones donde el progreso parcial es valioso
  • Necesitas un sistema de mocking sencillo para operaciones bulk que se integre con fflib

Complementariedad de los Frameworks

Es importante entender que Bulk DML Service Pattern no reemplaza a fflib Unit of Work, sino que lo complementa. Ambos frameworks pueden coexistir en el mismo proyecto:

  • Unit of Work para operaciones transaccionales críticas donde necesitas garantía total
  • Bulk DML Service para operaciones masivas donde el progreso parcial es aceptable y necesitas un sistema de mocking integrado con fflib

La clave está en usar el framework correcto para cada caso de uso específico.

Mejores Prácticas y Consideraciones

1. Validación Pre-DML

Siempre valida los datos antes de registrar:

public void processRecords(List<Account> accounts) {
    IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
    
    List<Account> validAccounts = new List<Account>();
    
    // Validar antes de registrar
    for (Account acc : accounts) {
        if (isValid(acc)) {
            validAccounts.add(acc);
        } else {
            Logger.warn(new LogMessage('Account {0} failed pre-DML validation', acc.Name));
        }
    }
    
    bulkDmlService.registerNew(validAccounts);
    IBulkDmlResult result = bulkDmlService.commitWork();
    // ...
}

2. Análisis de Resultados

Siempre analiza los resultados después de commitWork():

IBulkDmlResult result = bulkDmlService.commitWork();

if (!result.isAllSuccess()) {
    // Analizar fallos específicos
    processFailures(result);
    
    // Logging apropiado
    Logger.warn(new LogMessage('Partial success: {0}/{1} operations succeeded', 
        new List<Object>{result.getSuccessCount(), result.getTotalCount()}));
}

3. Manejo de Errores

Usa el logging estructurado para rastrear problemas:

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('Insert {0} failed: {1}', 
                new List<Object>{i, failedInserts[i].getErrors()[0].getMessage()}))
                .setDatabaseResult(new List<Database.SaveResult>{failedInserts[i]})
                .setRecord(records[i]);
        }
    }
}

4. Límites de Governor

Ten en cuenta que aunque uses allOrNone = false, sigues estando sujeto a los límites de governor de Salesforce:

  • Máximo 10,000 registros por operación DML
  • Límites de CPU y memoria
  • Límites de SOQL queries si los usas antes

5. Testing Completo

Escribe tests que cubran todos los escenarios:

@isTest
private class MyServiceTest {
    @isTest
    static void testSuccessScenario() {
        // Test con éxito total
    }
    
    @isTest
    static void testPartialSuccessScenario() {
        // Test con éxito parcial
    }
    
    @isTest
    static void testFailureScenario() {
        // Test con fallos totales
    }
    
    @isTest
    static void testMixedOperationsScenario() {
        // Test con operaciones mixtas
    }
}

Conclusión

El Bulk DML Service Pattern es un framework poderoso que complementa perfectamente a fflib Unit of Work, proporcionando una solución para escenarios donde necesitas operaciones DML resilientes con éxito parcial.

Ventajas Clave

  • API familiar: Similar a Unit of Work, fácil de adoptar
  • Resiliencia: Permite procesar grandes volúmenes con tolerancia a fallos
  • Observabilidad: Logging completo y resultados detallados
  • Testeable: Mock completo para pruebas unitarias verdaderas
  • Escalable: Maneja grandes volúmenes de datos eficientemente

Cuándo Usarlo

Este framework es especialmente útil en:

  • Procesamiento batch de grandes volúmenes
  • Migraciones de datos
  • Integraciones con sistemas externos
  • Procesamiento de datos importados
  • Cualquier escenario donde el progreso parcial es valioso

Próximos Pasos

Para implementar este patrón en tu organización:

  1. Asegúrate de tener fflib instalado: Este es un requisito previo, ya que el framework utiliza el Application factory de fflib para dependency injection
  2. Implementa las interfaces y clases del framework
  3. Integra con tu Application factory de fflib para dependency injection (mismo patrón que ya conoces)
  4. Configura tu sistema de logging preferido (puedes usar Nebula Logger como referencia: https://github.com/jongpie/NebulaLogger, o cualquier otro sistema de logging)
  5. Escribe tests usando el mock proporcionado que se integra perfectamente con fflib
  6. Comienza con casos de uso simples y escala gradualmente

El Bulk DML Service Pattern te permite construir sistemas más resilientes y manejables, especialmente cuando trabajas con grandes volúmenes de datos donde la perfección total no es realista pero el progreso parcial es valioso. Además, proporciona un sistema de mocking sencillo para operaciones bulk que se integra perfectamente con el ecosistema fflib que ya utilizas en tu proyecto.