Commencez à taper pour rechercher.

Salesforce Salesforce FFLib DML

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.

25 min de lecture

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éristiquefflib Unit of WorkBulk DML Service
APIregisterNew(), registerDirty(), registerUpsert()registerNew(), registerUpdate(), registerUpsert()
AtomicitéTotale (allOrNone = true)Partielle (allOrNone = false)
RésultatsExceptions en cas d’échecObjet de résultat détaillé
Cas d’utilisationTransactions critiquesTraitement en masse résilient
JournalisationBasiqueComplète et structurée (en utilisant votre système préféré)
TestsNécessite une base de donnéesMock complet disponible intégré à fflib
Injection de dépendancesFabrique d’applications fflibFabrique 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 :

  1. 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.
  2. Implémentez les interfaces et les classes du framework.
  3. Intégrez-vous à votre fabrique d’applications fflib pour l’injection de dépendances (le même modèle que vous connaissez déjà).
  4. 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).
  5. Écrivez des tests en utilisant le mock fourni qui s’intègre parfaitement à fflib.
  6. 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.