Bulk DML Service Pattern: Opérations DML Partielles dans Salesforce
Documentation technique complète du framework Bulk DML Service Pattern pour Salesforce. Apprenez à effectuer des opérations DML résilientes avec un succès partiel en utilisant une API familière de style Unit of Work.
Introduction : Le problème de l’Unit of Work et des opérations partielles
Lorsque nous travaillons avec fflib et son modèle Unit of Work, nous rencontrons une limitation importante : fflib_SObjectUnitOfWork utilise le DML avec allOrNone = true par défaut. Cela signifie que si un seul enregistrement échoue à la validation dans un lot de 200 enregistrements, toute la transaction est annulée et aucun enregistrement n’est enregistré.
Ce comportement est excellent pour garantir l’intégrité transactionnelle, mais il existe des scénarios où une approche différente est nécessaire :
- Traitement massif de données : Lorsque nous traitons de grands volumes de données et que nous voulons enregistrer les enregistrements valides même si certains échouent.
- Migrations de données : Dans les processus de migration où certains enregistrements peuvent échouer, mais nous ne voulons pas perdre tous les progrès.
- Intégrations : Lorsque nous recevons des données externes, dont certaines peuvent être invalides, mais nous voulons traiter les valides.
- Traitement par lots : Dans les classes de traitement par lots où nous voulons continuer même si certains enregistrements échouent.
fflib ne fournit pas de solution native pour ce cas d’utilisation, car l’Unit of Work est conçu pour des opérations transactionnelles complètes où tout ou rien doit être exécuté. De plus, l’une des principales raisons de la création de ce framework est de fournir un système de mocking simple pour les opérations en masse en utilisant le même modèle d’injection de dépendances que fflib, permettant d’écrire de véritables tests unitaires sans avoir besoin de toucher la base de données.
Le Bulk DML Service Pattern résout ces problèmes en fournissant une API familière de style Unit of Work mais avec un support pour les opérations partielles utilisant allOrNone = false, et un système de mocking complet qui s’intègre parfaitement à la fabrique d’applications fflib.
Qu’est-ce que le modèle de service DML en masse ?
Le Bulk DML Service Pattern est un framework conçu pour gérer les opérations DML (insert, update, upsert) dans Salesforce avec un succès partiel (allOrNone = false). Il fournit une API similaire à fflib_SObjectUnitOfWork mais avec la capacité de traiter les enregistrements de manière à ce que les échecs individuels ne bloquent pas toute l’opération.
Prérequis
⚠️ IMPORTANT : Ce framework nécessite que votre projet utilise fflib comme base architecturale. Le Bulk DML Service Pattern est conçu pour s’intégrer à la fabrique d’applications fflib, en utilisant le même modèle d’injection de dépendances pour faciliter le mocking et les tests unitaires.
Fonctionnalités Clés
- ✅ API familière : Méthodes similaires à l’Unit of Work (
registerNew,registerUpdate,registerUpsert,commitWork) - ✅ Opérations partielles : Permet à certains enregistrements d’échouer sans affecter les autres.
- ✅ Résultats détaillés : Fournit un résumé complet de toutes les opérations effectuées.
- ✅ Journalisation intégrée : Enregistre automatiquement les échecs et les succès (utilise votre système de journalisation préféré).
- ✅ Testable : Inclut un mock complet pour les tests unitaires sans base de données qui s’intègre à fflib.
- ✅ Gestion des erreurs : Exceptions personnalisées et journalisation structurée.
- ✅ Intégration fflib : Utilise la fabrique d’applications fflib pour l’injection de dépendances et le mocking.
Architecture du Framework
Le framework est construit sur une architecture basée sur des interfaces qui permet de facilement mocker pour les tests :
IBulkDmlService (Interface)
│
├── BulkDmlService (Implémentation réelle)
└── BulkDmlServiceMock (Mock pour les tests)
IBulkDmlResult (Interface)
│
└── BulkDmlResult (Implémentation)
Flux d’Opération
graph TD
A[Créer une instance de BulkDmlService] --> B[Enregistrer les enregistrements avec registerNew/Update/Upsert]
B --> C[Appeler commitWork]
C --> D[Exécuter les opérations DML avec allOrNone=false]
D --> E[Analyser les résultats]
E --> F[Créer BulkDmlResult]
F --> G[Effacer les enregistrements enregistrés]
G --> H[Retourner le résultat]
Composants Principaux
IBulkDmlService
L’interface principale définissant le contrat pour le service DML en masse :
public interface IBulkDmlService {
// Enregistrer les enregistrements pour l'insertion
void registerNew(List<SObject> records);
void registerNew(SObject record);
// Enregistrer les enregistrements pour la mise à jour
void registerUpdate(List<SObject> records);
void registerUpdate(SObject record);
// Enregistrer les enregistrements pour l'upsertion
void registerUpsert(List<SObject> records);
void registerUpsert(SObject record);
// Exécuter toutes les opérations enregistrées
IBulkDmlResult commitWork();
// Effacer les enregistrements sans exécution
void clear();
}
Caractéristiques de conception :
- Méthodes surchargées pour accepter à la fois des listes et des enregistrements individuels.
- Nomenclature cohérente avec fflib Unit of Work.
- Retour de résultats structurés pour une analyse ultérieure.
IBulkDmlResult
Interface qui encapsule les résultats de toutes les opérations DML :
public interface IBulkDmlResult {
Database.SaveResult[] getInsertResults();
Database.SaveResult[] getUpdateResults();
Database.UpsertResult[] getUpsertResults();
Boolean isAllSuccess();
Integer getSuccessCount();
Integer getFailureCount();
Integer getTotalCount();
}
Cette interface permet l’analyse programmatique des résultats et la prise de décision basée sur le succès ou l’échec des opérations.
Fonctionnement Interne : Comment le Framework Fonctionne
1. Stockage des Enregistrements
En interne, BulkDmlService maintient trois collections privées pour stocker les enregistrements enregistrés :
private List<SObject> recordsToInsert;
private List<SObject> recordsToUpdate;
private List<SObject> recordsToUpsert;
Lorsque vous appelez registerNew(), registerUpdate() ou registerUpsert(), les enregistrements sont ajoutés à ces collections respectives. Le service valide également que les enregistrements ne sont pas null avant de les ajouter et journalise chaque opération.
2. Processus de Commit
La méthode commitWork() est l’endroit où la magie opère :
public IBulkDmlResult commitWork() {
// 1. Journalisation initiale
Logger.info(new LogMessage('Commiting bulk DML work - Insert: {0}, Update: {1}, Upsert: {2}',
new List<Object>{recordsToInsert.size(), recordsToUpdate.size(), recordsToUpsert.size()}));
// 2. Initialiser les tableaux de résultats
Database.SaveResult[] insertResults = new List<Database.SaveResult>();
Database.SaveResult[] updateResults = new List<Database.SaveResult>();
Database.UpsertResult[] upsertResults = new List<Database.UpsertResult>();
try {
// 3. Exécuter les opérations DML avec allOrNone = false
if (!recordsToInsert.isEmpty()) {
insertResults = performInsert(recordsToInsert);
}
if (!recordsToUpdate.isEmpty()) {
updateResults = performUpdate(recordsToUpdate);
}
if (!recordsToUpsert.isEmpty()) {
upsertResults = performUpsert(recordsToUpsert);
}
// 4. Créer un résultat consolidé
IBulkDmlResult result = new BulkDmlResult(insertResults, updateResults, upsertResults);
// 5. Journalisation des résultats
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. Gestion des exceptions
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. Toujours effacer les enregistrements enregistrés après la tentative de commit
this.clear();
}
}
Remarque sur le système de journalisation : Dans l’implémentation de référence présentée dans cet article, Nebula Logger (https://github.com/jongpie/NebulaLogger) est utilisé, une solution d’observabilité robuste pour Salesforce. Cependant, le framework est complètement agnostique du système de journalisation utilisé. Vous pouvez intégrer n’importe quel système de journalisation préféré (Logger, System.debug, journalisation personnalisée, etc.) en adaptant les appels de journalisation dans l’implémentation à vos besoins.
3. Exécution de l’opération DML
Chaque opération DML est exécutée à l’aide de Database.insert(), Database.update() ou Database.upsert() avec le paramètre allOrNone = false :
@TestVisible
private Database.SaveResult[] performInsert(List<SObject> records) {
Logger.info(new LogMessage('Executing insert for {0} records', records.size()));
try {
// La clé est ici : allOrNone = false permet un succès partiel
Database.SaveResult[] results = Database.insert(records, false);
// Analyser et journaliser les résultats échoués
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);
}
}
Point clé : Le deuxième paramètre false dans Database.insert(records, false) est ce qui permet le succès partiel. Avec true, toute l’opération échouerait si un seul enregistrement pose problème.
4. Analyse des Résultats
Le framework analyse automatiquement les résultats après chaque opération DML :
@TestVisible
private void analyzeAndLogSaveResults(String operation, Database.SaveResult[] results, List<SObject> records) {
Database.SaveResult[] failedResults = new List<Database.SaveResult>();
// Collecter tous les résultats échoués
for (Database.SaveResult result : results) {
if (!result.isSuccess()) {
failedResults.add(result);
}
}
// Journaliser uniquement les échecs (les succès sont journalisés au niveau 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);
}
}
Remarque : Le code d’exemple utilise Nebula Logger, mais vous pouvez adapter les appels de journalisation à votre système préféré. Le framework ne dépend d’aucun système de journalisation spécifique.
5. Construction du Résultat
BulkDmlResult consolide tous les résultats en un seul objet :
public class BulkDmlResult implements IBulkDmlResult {
private Database.SaveResult[] insertResults;
private Database.SaveResult[] updateResults;
private Database.UpsertResult[] upsertResults;
public Integer getSuccessCount() {
Integer successCount = 0;
// Compter les insertions réussies
for (Database.SaveResult result : insertResults) {
if (result.isSuccess()) {
successCount++;
}
}
// Compter les mises à jour réussies
for (Database.SaveResult result : updateResults) {
if (result.isSuccess()) {
successCount++;
}
}
// Compter les upserts réussis
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;
}
}
Exemples d’Utilisation Pratiques
Exemple 1 : Traitement de données en masse
Imaginez un scénario où vous devez traiter 1000 Leads reçus d’une intégration externe. Certains peuvent contenir des données invalides, mais vous voulez enregistrer les valides :
public class LeadImportService {
public void processLeads(List<Lead> leadsToProcess) {
// Obtenir le service de la fabrique d'applications (supporte le mocking)
IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
List<Lead> validLeads = new List<Lead>();
List<Lead> invalidLeads = new List<Lead>();
// Séparer les leads valides des invalides
for (Lead lead : leadsToProcess) {
if (isValidLead(lead)) {
validLeads.add(lead);
} else {
invalidLeads.add(lead);
}
}
// Enregistrer uniquement les leads valides
bulkDmlService.registerNew(validLeads);
// Exécuter les opérations
IBulkDmlResult result = bulkDmlService.commitWork();
// Analyser les résultats
if (!result.isAllSuccess()) {
Logger.warn(new LogMessage('Succès partiel lors de l'importation des leads. {0} réussis, {1} échoués sur {2} au total',
new List<Object>{result.getSuccessCount(), result.getFailureCount(), result.getTotalCount()}))
.setDatabaseResult(result.getInsertResults());
// Traiter les erreurs spécifiques si nécessaire
processFailedLeads(result.getInsertResults(), validLeads);
}
// Notifier les leads invalides qui n'ont même pas été tentés d'être traités
if (!invalidLeads.isEmpty()) {
Logger.warn(new LogMessage('{0} leads ont été rejetés avant le DML en raison d'erreurs de validation',
invalidLeads.size()));
}
}
private Boolean isValidLead(Lead lead) {
// Votre logique de validation ici
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();
// Traiter chaque erreur
for (Database.Error error : errors) {
Logger.error(new LogMessage('Le lead {0} a échoué : {1}',
new List<Object>{failedLead.Email, error.getMessage()}))
.setRecord(failedLead);
}
}
}
}
}
Exemple 2 : Mise à jour en masse avec des validations
Mettre à jour plusieurs comptes, mais permettre à certains d’échouer :
public class AccountUpdateService {
public void updateAccountsBasedOnCriteria(List<Account> accounts) {
IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
List<Account> accountsToUpdate = new List<Account>();
// Appliquer la logique métier pour déterminer ce qu'il faut mettre à jour
for (Account acc : accounts) {
if (acc.AnnualRevenue > 1000000) {
acc.Type = 'Enterprise';
accountsToUpdate.add(acc);
}
}
// Enregistrer pour la mise à jour
bulkDmlService.registerUpdate(accountsToUpdate);
// Exécuter
IBulkDmlResult result = bulkDmlService.commitWork();
// Journalisation des résultats
Logger.info(new LogMessage('Mise à jour réussie de {0} comptes sur {1}',
new List<Object>{result.getSuccessCount(), result.getTotalCount()}));
}
}
Exemple 3 : Opérations mixtes (Insert, Update, Upsert)
Un cas plus complexe où vous devez effectuer plusieurs types d’opérations :
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>();
// Classer les contacts en fonction de leur statut
for (Contact contact : contactsToSync) {
if (contact.Id == null) {
newContacts.add(contact);
} else if (shouldUpdate(contact)) {
existingContacts.add(contact);
} else {
contactsToUpsert.add(contact);
}
}
// Enregistrer toutes les opérations
bulkDmlService.registerNew(newContacts);
bulkDmlService.registerUpdate(existingContacts);
bulkDmlService.registerUpsert(contactsToUpsert);
// Exécuter toutes les opérations dans une seule transaction
IBulkDmlResult result = bulkDmlService.commitWork();
// Analyser les résultats par type d'opération
analyzeResults(result, newContacts, existingContacts, contactsToUpsert);
}
private Boolean shouldUpdate(Contact contact) {
// Logique pour déterminer si elle doit être mise à jour
return contact.LastModifiedDate < DateTime.now().addHours(-24);
}
private void analyzeResults(IBulkDmlResult result, List<Contact> inserts,
List<Contact> updates, List<Contact> upserts) {
// Analyser les insertions
if (!result.getInsertResults().isEmpty()) {
Logger.info(new LogMessage('Insertions : {0} réussies, {1} échouées',
new List<Object>{countSuccesses(result.getInsertResults()),
countFailures(result.getInsertResults())}));
}
// Analyser les mises à jour
if (!result.getUpdateResults().isEmpty()) {
Logger.info(new LogMessage('Mises à jour : {0} réussies, {1} échouées',
new List<Object>{countSuccesses(result.getUpdateResults()),
countFailures(result.getUpdateResults())}));
}
// Analyser les upserts
if (!result.getUpsertResults().isEmpty()) {
Logger.info(new LogMessage('Upserts : {0} réussis, {1} échoués',
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;
}
}
Tests et Mocking : Tests Unitaires sans Base de Données
L’une des principales raisons de la création de ce framework est de fournir un système de mocking simple pour les opérations en masse en utilisant le même modèle d’injection de dépendances que fflib. Cela permet d’écrire de véritables tests unitaires sans avoir besoin de toucher la base de données, en suivant les mêmes pratiques que vous connaissez déjà avec fflib.
Le système de mocking est entièrement intégré à la fabrique d’applications fflib, vous permettant d’utiliser le même modèle familier pour injecter des mocks dans vos tests.
BulkDmlServiceMock
Le mock inclut des capacités avancées pour simuler différents scénarios :
@isTest
private class LeadImportServiceTest {
@isTest
static void testProcessLeadsWithPartialSuccess() {
// Étant donné
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')
};
// Configurer le mock pour un succès partiel
BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock()
.withPartialSuccess(2); // 2 succès, 1 échec
Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
LeadImportService service = new LeadImportService();
// Quand
Test.startTest();
service.processLeads(testLeads);
Test.stopTest();
// Alors
// Vérifier que les leads ont été enregistrés
System.assertEquals(3, mockBulkDmlService.registeredInserts.size(),
'Tous les leads devraient être enregistrés');
// Vérifier que commitWork a été appelé
System.assertEquals(1, mockBulkDmlService.commitWorkCallCount,
'commitWork devrait être appelé une fois');
// Vérifier les résultats
IBulkDmlResult result = mockBulkDmlService.commitResults[0];
System.assertEquals(2, result.getSuccessCount(),
'Deux leads devraient réussir');
System.assertEquals(1, result.getFailureCount(),
'Un lead devrait échouer');
System.assertFalse(result.isAllSuccess(),
'Toutes les opérations ne devraient pas réussir');
}
}
Simulation d’Erreurs Personnalisées
Le mock vous permet de simuler des erreurs spécifiques :
@isTest
private class LeadImportServiceErrorTest {
@isTest
static void testProcessLeadsWithCustomErrors() {
// Étant donné
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')
};
// Configurer le mock avec des erreurs personnalisées
BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock()
.withPartialSuccess(1) // 1 succès, 1 échec
.withErrorMessages(new List<String>{'Échec de validation personnalisée'})
.withErrorCodes(new List<System.StatusCode>{System.StatusCode.FIELD_CUSTOM_VALIDATION_EXCEPTION});
Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
LeadImportService service = new LeadImportService();
// Quand
Test.startTest();
service.processLeads(testLeads);
Test.stopTest();
// Alors
IBulkDmlResult result = mockBulkDmlService.commitResults[0];
Database.SaveResult[] insertResults = result.getInsertResults();
// Vérifier que la deuxième insertion a échoué avec l'erreur personnalisée
System.assertFalse(insertResults[1].isSuccess(),
'La deuxième insertion aurait dû échouer');
System.assertEquals('Échec de validation personnalisée',
insertResults[1].getErrors()[0].getMessage(),
'Le message d'erreur devrait correspondre');
System.assertEquals(System.StatusCode.FIELD_CUSTOM_VALIDATION_EXCEPTION,
insertResults[1].getErrors()[0].getStatusCode(),
'Le code d'erreur devrait correspondre');
}
}
Vérification des Interactions
Le mock permet également de vérifier que les méthodes correctes ont été appelées :
@isTest
private class ContactSyncServiceTest {
@isTest
static void testSyncContactsWithMixedOperations() {
// Étant donné
Id accountId = fflib_IDGenerator.generate(Account.SObjectType);
List<Contact> contacts = new List<Contact>{
new Contact(LastName = 'Nouveau', AccountId = accountId), // Nouveau
new Contact(Id = fflib_IDGenerator.generate(Contact.SObjectType),
LastName = 'Existant', AccountId = accountId), // Existant
new Contact(LastName = 'Upsert', AccountId = accountId) // Upsert
};
BulkDmlServiceMock mockBulkDmlService = new BulkDmlServiceMock().withAllSuccess();
Application.Service.setMock(IBulkDmlService.class, mockBulkDmlService);
ContactSyncService service = new ContactSyncService();
// Quand
Test.startTest();
service.syncContacts(contacts);
Test.stopTest();
// Alors - Vérifier que toutes les méthodes d'enregistrement ont été appelées
mockBulkDmlService.verifyCalls(1, 1, 1, 1); // 1 registerNew, 1 registerUpdate, 1 registerUpsert, 1 commitWork
}
}
Cas d’Utilisation Avancés
Cas d’Utilisation 1 : Traitement par lots avec Résilience
Dans une classe de traitement par lots, vous pouvez utiliser le Bulk DML Service pour traiter de grands volumes de données avec résilience :
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>();
// Traiter chaque compte
for (Account acc : accounts) {
// Appliquer les transformations
acc.Processed__c = true;
acc.ProcessedDate__c = DateTime.now();
accountsToUpdate.add(acc);
}
// Enregistrer et exécuter
bulkDmlService.registerUpdate(accountsToUpdate);
IBulkDmlResult result = bulkDmlService.commitWork();
// Journalisation pour la surveillance
if (!result.isAllSuccess()) {
Logger.warn(new LogMessage('Lot traité {0} comptes sur {1} avec succès',
new List<Object>{result.getSuccessCount(), result.getTotalCount()}))
.setDatabaseResult(result.getUpdateResults());
}
}
public void finish(Database.BatchableContext bc) {
// Logique de finalisation
}
}
Cas d’Utilisation 2 : Intégration avec la logique de Réessai
Combiner le Bulk DML Service avec la logique de réessai :
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('Tous les enregistrements ont été traités avec succès à la tentative {0}', attempt));
break;
}
// Identifier les enregistrements échoués pour le réessai
recordsToProcess = identifyFailedRecords(result.getInsertResults(), recordsToProcess);
if (!recordsToProcess.isEmpty()) {
Logger.warn(new LogMessage('La tentative {0} a échoué. Réessai de {1} enregistrements',
new List<Object>{attempt, recordsToProcess.size()}));
}
}
if (!recordsToProcess.isEmpty()) {
Logger.error(new LogMessage('Échec du traitement de {0} enregistrements après {1} tentatives',
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;
}
}
Comparaison avec fflib Unit of Work
| Caractéristique | fflib Unit of Work | Bulk DML Service |
|---|---|---|
| API | registerNew(), registerDirty(), registerUpsert() | registerNew(), registerUpdate(), registerUpsert() |
| Atomicité | Totale (allOrNone = true) | Partielle (allOrNone = false) |
| Résultats | Exceptions en cas d’échec | Objet de résultat détaillé |
| Cas d’utilisation | Transactions critiques | Traitement en masse résilient |
| Journalisation | Basique | Complète et structurée (en utilisant votre système préféré) |
| Tests | Nécessite une base de données | Mock complet disponible intégré à fflib |
| Injection de dépendances | Fabrique d’applications fflib | Fabrique d’applications fflib (même modèle) |
Quand utiliser chacun ?
Utiliser fflib Unit of Work lorsque :
- Vous avez besoin d’une garantie transactionnelle complète.
- Les opérations sont critiques pour l’entreprise.
- Un seul échec doit annuler toute l’opération.
- Vous travaillez avec des relations complexes qui nécessitent de l’intégrité.
Utiliser Bulk DML Service lorsque :
- Vous traitez de grands volumes de données.
- Vous pouvez tolérer des échecs individuels.
- Vous avez besoin de résilience dans les intégrations.
- Vous effectuez des migrations où des progrès partiels sont précieux.
- Vous avez besoin d’un système de mocking simple pour les opérations en masse qui s’intègre à fflib.
Complémentarité des Frameworks
Il est important de comprendre que le Bulk DML Service Pattern ne remplace pas fflib Unit of Work, mais le complète. Les deux frameworks peuvent coexister dans le même projet :
- Unit of Work pour les opérations transactionnelles critiques où vous avez besoin d’une garantie complète.
- Bulk DML Service pour les opérations en masse où le succès partiel est acceptable et vous avez besoin d’un système de mocking intégré à fflib.
La clé est d’utiliser le bon framework pour chaque cas d’utilisation spécifique.
Bonnes Pratiques et Considérations
1. Validation Pré-DML
Toujours valider les données avant l’enregistrement :
public void processRecords(List<Account> accounts) {
IBulkDmlService bulkDmlService = (IBulkDmlService) Application.Service.newInstance(IBulkDmlService.class);
List<Account> validAccounts = new List<Account>();
// Valider avant d'enregistrer
for (Account acc : accounts) {
if (isValid(acc)) {
validAccounts.add(acc);
} else {
Logger.warn(new LogMessage('Le compte {0} a échoué la validation pré-DML', acc.Name));
}
}
bulkDmlService.registerNew(validAccounts);
IBulkDmlResult result = bulkDmlService.commitWork();
// ...
}
2. Analyse des Résultats
Toujours analyser les résultats après commitWork() :
IBulkDmlResult result = bulkDmlService.commitWork();
if (!result.isAllSuccess()) {
// Analyser les échecs spécifiques
processFailures(result);
// Journalisation appropriée
Logger.warn(new LogMessage('Succès partiel : {0}/{1} opérations ont réussi',
new List<Object>{result.getSuccessCount(), result.getTotalCount()}));
}
3. Gestion des Erreurs
Utilisez une journalisation structurée pour suivre les problèmes :
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('L'insertion {0} a échoué : {1}',
new List<Object>{i, failedInserts[i].getErrors()[0].getMessage()}))
.setDatabaseResult(new List<Database.SaveResult>{failedInserts[i]})
.setRecord(records[i]);
}
}
}
4. Limites du Gouverneur
Gardez à l’esprit que même avec allOrNone = false, vous êtes toujours soumis aux limites du gouverneur de Salesforce :
- Maximum 10 000 enregistrements par opération DML.
- Limites de CPU et de mémoire.
- Limites des requêtes SOQL si utilisées auparavant.
5. Tests Complets
Écrivez des tests qui couvrent tous les scénarios :
@isTest
private class MyServiceTest {
@isTest
static void testSuccessScenario() {
// Test avec succès total.
}
@isTest
static void testPartialSuccessScenario() {
// Test avec succès partiel.
}
@isTest
static void testFailureScenario() {
// Test avec échecs totaux.
}
@isTest
static void testMixedOperationsScenario() {
// Test avec des opérations mixtes.
}
}
Conclusion
Le Bulk DML Service Pattern est un framework puissant qui complète parfaitement l’fflib Unit of Work, offrant une solution pour les scénarios où vous avez besoin d’opérations DML résilientes avec un succès partiel.
Avantages Clés
- ✅ API familière : Similaire à l’Unit of Work, facile à adopter.
- ✅ Résilience : Permet de traiter de grands volumes avec tolérance aux pannes.
- ✅ Observabilité : Journalisation complète et résultats détaillés.
- ✅ Testable : Mock complet pour de véritables tests unitaires.
- ✅ Évolutif : Gère efficacement de grands volumes de données.
Quand l’utiliser
Ce framework est particulièrement utile dans :
- Le traitement par lots de grands volumes.
- Les migrations de données.
- Les intégrations avec des systèmes externes.
- Le traitement des données importées.
- Tout scénario où un progrès partiel est précieux.
Prochaines étapes
Pour implémenter ce modèle dans votre organisation :
- Assurez-vous que fflib est installé : C’est un prérequis, car le framework utilise la fabrique d’applications fflib pour l’injection de dépendances.
- Implémentez les interfaces et les classes du framework.
- Intégrez-vous à votre fabrique d’applications fflib pour l’injection de dépendances (le même modèle que vous connaissez déjà).
- Configurez votre système de journalisation préféré (vous pouvez utiliser Nebula Logger comme référence : https://github.com/jongpie/NebulaLogger, ou tout autre système de journalisation).
- Écrivez des tests en utilisant le mock fourni qui s’intègre parfaitement à fflib.
- Commencez par des cas d’utilisation simples et adaptez-vous progressivement.
Le Bulk DML Service Pattern vous permet de construire des systèmes plus résilients et gérables, surtout lorsque vous travaillez avec de grands volumes de données où la perfection totale n’est pas réaliste, mais les progrès partiels sont précieux. De plus, il fournit un système de mocking simple pour les opérations en masse qui s’intègre parfaitement à l’écosystème fflib que vous utilisez déjà dans votre projet.
Articles Similaires
Salesforce En vedette Query Plan Architecture : Pattern pour les Triggers Apex
Découvrez comment structurer des triggers complexes dans Salesforce avec le pattern Query Plan Architecture. Séparez la logique en 4 phases (Collect, Load, Run, Commit) pour éliminer les SOQL dupliqués, respecter les governor limits et faciliter les tests.
Salesforce Agentforce : La Révolution de l'IA dans Salesforce
Découvrez Agentforce, la nouvelle plateforme d'agents IA autonomes de Salesforce. Apprenez à créer, configurer et déployer des agents intelligents pour automatiser les processus métier.