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.
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ística | fflib Unit of Work | Bulk DML Service |
|---|---|---|
| API | registerNew(), registerDirty(), registerUpsert() | registerNew(), registerUpdate(), registerUpsert() |
| Atomicidad | Total (allOrNone = true) | Parcial (allOrNone = false) |
| Resultados | Excepciones en fallo | Objeto de resultados detallado |
| Casos de uso | Transacciones críticas | Procesamiento 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:
- Asegúrate de tener fflib instalado: Este es un requisito previo, ya que el framework utiliza el Application factory de fflib para dependency injection
- Implementa las interfaces y clases del framework
- Integra con tu Application factory de fflib para dependency injection (mismo patrón que ya conoces)
- Configura tu sistema de logging preferido (puedes usar Nebula Logger como referencia: https://github.com/jongpie/NebulaLogger, o cualquier otro sistema de logging)
- Escribe tests usando el mock proporcionado que se integra perfectamente con fflib
- 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.
Artículos Relacionados
Salesforce Arquitectura Salesforce con FFLib: Patrones Enterprise
Guía completa sobre la implementación de patrones arquitectónicos enterprise en Salesforce utilizando FFLib. Incluye Domain Layer, Selector Layer, Service Layer y Unit of Work.
Salesforce Destacado Query Plan Architecture: Patrón para Triggers Apex
Descubre cómo estructurar triggers complejos en Salesforce con el patrón Query Plan Architecture. Separa la lógica en 4 fases (Collect, Load, Run, Commit) para eliminar SOQL duplicados, respetar governor limits y facilitar el testing.
Salesforce Destacado CI/CD para Salesforce: Automatización de Despliegues
Implementación completa de pipelines CI/CD para Salesforce usando GitHub Actions, SFDX CLI y Scratch Orgs. Incluye mejores prácticas y troubleshooting.