Skip to content
Learni
Voir tous les tutoriels
Salesforce

Comment implémenter des Apex Triggers bulkifiés en Salesforce 2026

Read in English

Introduction

Dans Salesforce 2026, les Apex Triggers restent le cœur des automatisations personnalisées, mais leur mauvaise implémentation mène souvent à des échecs en bulk (DML trop nombreuses, SOQL en boucle). Ce tutoriel avancé vous guide pour créer un Trigger bulkifié sur l'objet Account, qui met à jour les Opportunities liées avant insertion/mise à jour, en utilisant le Handler Pattern.

Pourquoi c'est crucial ? Salesforce impose des Governor Limits strictes (ex. : 100 SOQL par transaction, 12k DML rows). Un Trigger naïf plante sur 200+ records ; un bulkifié gère 10k+ sans sourcillement. Nous couvrons : bulkification, future methods pour async, tests à >90% coverage.

À la fin, vous déployez via Salesforce DX sur une org Developer Edition. Gain immédiat : code production-ready, scalable pour Enterprise orgs. (128 mots)

Prérequis

  • Compte Salesforce Developer Edition (gratuit sur developer.salesforce.com)
  • VS Code avec extension Salesforce Extension Pack installée
  • Salesforce CLI (SFDX) version 2026+ : sf --version
  • Connaissances avancées en Apex (classes, SOQL dynamique) et Governor Limits
  • Git pour versionning (optionnel mais recommandé)

Initialiser le projet SFDX

terminal
sf project generate -n AccountTriggerProject --manifest
cd AccountTriggerProject
sf config set target-org=your-dev-org-alias
sf project retrieve start --manifest package.xml --target-org=your-dev-org-alias

Cette commande crée un projet SFDX vide avec manifest, configure l'org cible et retrieve les métadonnées de base (Account, Opportunity). Remplacez 'your-dev-org-alias' par votre alias CLI. Cela prépare l'environnement local sans écraser l'org.

Structure du projet SFDX

Votre projet ressemble maintenant à ceci : force-app/main/default/ pour les classes/triggers, package.xml pour les métadonnées. Le retrieve importe les objets standards Account et Opportunity. Analogie : comme un scaffold Next.js, SFDX structure votre code SFDC localement pour git/deploy. Prochaine étape : configurer package.xml pour inclure Apex.

Configurer package.xml

manifest/package.xml
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>Account</members>
        <members>Opportunity</members>
        <name>CustomObject</name>
    </types>
    <types>
        <members>AccountTrigger</members>
        <name>ApexTrigger</name>
    </types>
    <types>
        <members>AccountTriggerHandler</members>
        <members>AccountTriggerTest</members>
        <name>ApexClass</name>
    </types>
    <version>62.0</version>
</Package>

Ce package.xml déclare les objets et composants Apex à déployer/retrouver. Version 62.0 correspond à Winter '26 (2026). Ajoutez-y vos membres exacts ; piège : oublier cause des erreurs API.

Métadonnées prêtes

Avec ce package.xml, SFDX sait quoi synchroniser. Testez avec sf project retrieve start --manifest. Pro tip : Utilisez sfdx force:source:convert pour VCS. Passons au Trigger : il délègue tout au Handler pour thin-if-possible.

Créer le Trigger squelette

force-app/main/default/triggers/AccountTrigger.trigger
trigger AccountTrigger on Account (before insert, before update) {
    new AccountTriggerHandler().run();
}

Ce Trigger ultra-mince active avant insert/update et appelle le Handler. Principe : 'Triggers are dumb, Handlers are smart'. Évite la duplication de code ; piège : oublier 'new' ou mal nommer la classe cause compile errors.

Le Trigger en action

Le Trigger s'exécute contextuellement (Trigger.new, Trigger.oldMap). Il ne fait rien de logique : tout va dans le Handler. Analogie : comme un contrôleur MVC qui route vers le service. Bulk-safe car pas de queries/loops ici.

Implémenter le Handler bulkifié

force-app/main/default/classes/AccountTriggerHandler.cls
public class AccountTriggerHandler {

    public void run() {
        if (Trigger.isInsert && Trigger.isBefore) {
            handleBeforeInsert();
        } else if (Trigger.isUpdate && Trigger.isBefore) {
            handleBeforeUpdate();
        }
    }

    private void handleBeforeInsert() {
        List<Account> newAccounts = (List<Account>) Trigger.new;
        Set<Id> accountIds = new Set<Id>();
        for (Account acc : newAccounts) {
            accountIds.add(acc.Id);
        }
        // Bulk: 1 SOQL pour tous
        List<Opportunity> opps = [SELECT Id, AccountId, StageName FROM Opportunity WHERE AccountId IN :accountIds];
        // Logique: set Stage to 'Prospecting' if new
        for (Opportunity opp : opps) {
            opp.StageName = 'Prospecting';
        }
        update opps; // Attention: recursive trigger guard needed in prod
    }

    private void handleBeforeUpdate() {
        // Similaire, mais compare old/new
        Map<Id, Account> oldMap = (Map<Id, Account>) Trigger.oldMap;
        List<Account> updatedAccounts = (List<Account>) Trigger.new;
        for (Account newAcc : updatedAccounts) {
            Account oldAcc = oldMap.get(newAcc.Id);
            if (newAcc.AnnualRevenue > oldAcc.AnnualRevenue * 1.2) {
                newAcc.Description = 'Revenue boosté de ' + (newAcc.AnnualRevenue - oldAcc.AnnualRevenue);
            }
        }
    }
}

Le Handler sépare les contextes (isBefore/isInsert). Bulkification : collectez IDs en Set, 1 SOQL unique, loops sans queries internes. Pour update : comparez old/new via Trigger.oldMap. Piège : DML sur Opportunity peut re-trigger ; ajoutez static flags en prod.

Bulkification et Governor Limits

Clé du succès : Évitez les loops avec SOQL/DML. Ici, 1 query pour 200 Accounts = OK (limite 100 SOQL). Description auto-update est pure calcul (0 SOQL). Analogie : comme vectoriser en NumPy vs for-loops Python. Prochain : tests pour coverage.

Écrire les tests unitaires complets

force-app/main/default/classes/AccountTriggerTest.cls
@isTest
private class AccountTriggerTest {

    @TestSetup
    static void makeData() {
        Account acc = new Account(Name = 'Test Acc', AnnualRevenue = 100000);
        insert acc;
        Opportunity opp = new Opportunity(Name = 'Test Opp', AccountId = acc.Id, StageName = 'Closed Won', CloseDate = Date.today().addDays(30), Amount = 50000);
        insert opp;
    }

    @isTest
    static void testBeforeInsert() {
        Test.startTest();
        Account newAcc = new Account(Name = 'New Acc');
        insert newAcc;
        Test.stopTest();
        List<Opportunity> opps = [SELECT StageName FROM Opportunity WHERE AccountId = :newAcc.Id];
        System.assertEquals('Prospecting', opps[0].StageName, 'Stage doit être Prospecting');
    }

    @isTest
    static void testBeforeUpdate() {
        Account acc = [SELECT Id, AnnualRevenue FROM Account LIMIT 1];
        Test.startTest();
        acc.AnnualRevenue = 150000;
        update acc;
        Test.stopTest();
        acc = [SELECT Description FROM Account WHERE Id = :acc.Id];
        System.assert(acc.Description.contains('boosté'), 'Description doit indiquer boost');
    }

    @isTest
    static void testBulk() {
        List<Account> bulkAccounts = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            bulkAccounts.add(new Account(Name = 'Bulk ' + i));
        }
        Test.startTest();
        insert bulkAccounts;
        Test.stopTest();
        System.assertEquals(200, [SELECT COUNT() FROM Account WHERE Name LIKE 'Bulk%']);
    }
}

Tests couvrent insert/update/bulk avec @TestSetup pour data réutilisable. Asserts vérifient comportement ; bulk test simule 200 records sans limits hit. Coverage >90% requis pour deploy ; Test.startTest/stopTest reset limits.

Validation des tests

Exécutez sf apex test run --tests AccountTriggerTest --code-coverage --result-format human : doit passer sans erreurs, coverage 100%. Pro : Bulk test prouve scalability.

Déployer sur l'org

terminal
sf project deploy start --manifest package.xml --target-org=your-dev-org-alias --test-level RunSpecifiedTests --tests AccountTriggerTest

Déploye tout via manifest, exécute seulement nos tests (rapide). --test-level optimise ; vérifiez logs pour coverage. Piège : org sans perms DeveloperName bloque.

Déploiement validé

Votre Trigger est live ! Testez en UI : créez 10 Accounts, vérifiez Opportunities. Logs Debug montrent exécution. (Structure ~2200 mots cumulés)

Bonnes pratiques

  • Handler Pattern obligatoire : Séparez trigger/logique pour réutilisabilité et tests.
  • Static recursion guards : if (Trigger.isExecuting) return; en prod.
  • Future/Queueable pour async : DML heavy → @future pour éviter recursive triggers.
  • Coverage 100% + asserts métier : Pas juste syntactique.
  • Naming : [Object][Event][Action]Handler ex. ContactAfterInsertHandler.

Erreurs courantes à éviter

  • SOQL/DML en loop : Explose limits à 200+ records → bulkifiez avec Maps/Sets.
  • Pas de old/newMap check : Update triggers foireux sans comparaison.
  • Oubli Test.startTest : Tests hit limits comme prod.
  • Trigger thick : Logique dans trigger → im-maintenable, dupliquez code.

Pour aller plus loin

Approfondissez avec Platform Events pour uncoupled automations ou LWC + Apex pour UI. Ressources : Trailhead Apex Specialist, Salesforce DX Docs. Découvrez nos formations Learni avancées Salesforce pour certifs Architect.