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
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-aliasCette 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
<?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
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
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é
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
@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
sf project deploy start --manifest package.xml --target-org=your-dev-org-alias --test-level RunSpecifiedTests --tests AccountTriggerTestDé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 →
@futurepour é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.