-
[Salesforce Developer] Trigger Handler FrameworkSalesforce 2025. 2. 24. 21:51
- TriggerHandler를 상속받은 AccountTriggerHandler.cls를 AccountTrigger.trigger가 실행하는 구조
- TriggerHandler : 상속과 오버라이드를 위해 virtual class를 사용, run으로 호출되면 생성자가 실행되면서 context 설정
public virtual class TriggerHandler { // static map of handlername, times run() was invoked private static Map<String, LoopCount> loopCountMap; private static Set<String> bypassedHandlers; // the current context of the trigger, overridable in tests @TestVisible private TriggerContext context; // the current context of the trigger, overridable in tests @TestVisible private Boolean isTriggerExecuting; // static initialization static { loopCountMap = new Map<String, LoopCount>(); bypassedHandlers = new Set<String>(); } // constructor public TriggerHandler() { this.setTriggerContext(); } /*************************************** * public instance methods ***************************************/ // main method that will be called during execution public void run() { if(!validateRun()) { return; } addToLoopCount(); // dispatch to the correct handler method switch on this.context { when BEFORE_INSERT { this.beforeInsert(); } when BEFORE_UPDATE { this.beforeUpdate(); } when BEFORE_DELETE { this.beforeDelete(); } when AFTER_INSERT { this.afterInsert(); } when AFTER_UPDATE { this.afterUpdate(); } when AFTER_DELETE { this.afterDelete(); } when AFTER_UNDELETE { this.afterUndelete(); } } } public void setMaxLoopCount(Integer max) { String handlerName = getHandlerName(); if(!TriggerHandler.loopCountMap.containsKey(handlerName)) { TriggerHandler.loopCountMap.put(handlerName, new LoopCount(max)); } else { TriggerHandler.loopCountMap.get(handlerName).setMax(max); } } public void clearMaxLoopCount() { this.setMaxLoopCount(-1); } /*************************************** * public static methods ***************************************/ public static void bypass(String handlerName) { TriggerHandler.bypassedHandlers.add(handlerName); } public static void clearBypass(String handlerName) { TriggerHandler.bypassedHandlers.remove(handlerName); } public static Boolean isBypassed(String handlerName) { return TriggerHandler.bypassedHandlers.contains(handlerName); } public static void clearAllBypasses() { TriggerHandler.bypassedHandlers.clear(); } /*************************************** * private instancemethods ***************************************/ @TestVisible private void setTriggerContext() { this.setTriggerContext(null, false); } @TestVisible private void setTriggerContext(String ctx, Boolean testMode) { if(!Trigger.isExecuting && !testMode) { this.isTriggerExecuting = false; return; } else { this.isTriggerExecuting = true; } if((Trigger.isExecuting && Trigger.isBefore && Trigger.isInsert) || (ctx != null && ctx == 'before insert')) { this.context = TriggerContext.BEFORE_INSERT; } else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isUpdate) || (ctx != null && ctx == 'before update')){ this.context = TriggerContext.BEFORE_UPDATE; } else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isDelete) || (ctx != null && ctx == 'before delete')) { this.context = TriggerContext.BEFORE_DELETE; } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isInsert) || (ctx != null && ctx == 'after insert')) { this.context = TriggerContext.AFTER_INSERT; } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUpdate) || (ctx != null && ctx == 'after update')) { this.context = TriggerContext.AFTER_UPDATE; } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isDelete) || (ctx != null && ctx == 'after delete')) { this.context = TriggerContext.AFTER_DELETE; } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUndelete) || (ctx != null && ctx == 'after undelete')) { this.context = TriggerContext.AFTER_UNDELETE; } } // increment the loop count @TestVisible private void addToLoopCount() { String handlerName = getHandlerName(); if(TriggerHandler.loopCountMap.containsKey(handlerName)) { Boolean exceeded = TriggerHandler.loopCountMap.get(handlerName).increment(); if(exceeded) { Integer max = TriggerHandler.loopCountMap.get(handlerName).max; throw new TriggerHandlerException('Maximum loop count of ' + String.valueOf(max) + ' reached in ' + handlerName); } } } // make sure this trigger should continue to run @TestVisible private Boolean validateRun() { if(!this.isTriggerExecuting || this.context == null) { throw new TriggerHandlerException('Trigger handler called outside of Trigger execution'); } return !TriggerHandler.bypassedHandlers.contains(getHandlerName()); } @TestVisible private String getHandlerName() { return this.toString().substringBefore(':'); } /*************************************** * context methods ***************************************/ // context-specific methods for override @TestVisible protected virtual void beforeInsert(){} @TestVisible protected virtual void beforeUpdate(){} @TestVisible protected virtual void beforeDelete(){} @TestVisible protected virtual void afterInsert(){} @TestVisible protected virtual void afterUpdate(){} @TestVisible protected virtual void afterDelete(){} @TestVisible protected virtual void afterUndelete(){} /*************************************** * inner classes ***************************************/ // inner class for managing the loop count per handler @TestVisible private class LoopCount { private Integer max; private Integer count; public LoopCount() { this.max = 5; this.count = 0; } public LoopCount(Integer max) { this.max = max; this.count = 0; } public Boolean increment() { this.count++; return this.exceeded(); } public Boolean exceeded() { return this.max >= 0 && this.count > this.max; } public Integer getMax() { return this.max; } public Integer getCount() { return this.count; } public void setMax(Integer max) { this.max = max; } } // possible trigger contexts @TestVisible private enum TriggerContext { BEFORE_INSERT, BEFORE_UPDATE, BEFORE_DELETE, AFTER_INSERT, AFTER_UPDATE, AFTER_DELETE, AFTER_UNDELETE } // exception class public class TriggerHandlerException extends Exception {} }- AccountTriggerHandler.cls
public class AccountTriggerHandler extends TriggerHandler{ this.setMaxLoopCount(1); // Prevent Recursion/loop public override void beforeUpdate(){ Map<Id, Account> newRecords = Trigger.newMap; Map<Id, Account> oldRecords = Trigger.oldMap; //code .... } }- AccountTrigger.trigger
trigger AccountTrigger on Account (before insert, before update, before delete, after insert, after update, after delete, after undelete) { new AccountTriggerHandler().run(); }- trigger pass
TriggerHandler.bypass('AccountTriggerHandler'); acc.Name = 'No Trigger'; update acc; // won't invoke the AccountTriggerHandler TriggerHandler.clearBypass('AccountTriggerHandler');출처)
https://medium.com/@mayankdhanopia/salesforce-trigger-handler-framework-2ac0c5c44edf
'Salesforce' 카테고리의 다른 글
[Salesforce Administrator] Flow (0) 2025.02.28 [Salesforce Developer] lightning-datatable (0) 2025.02.28 [Salesforce Administrator] Approval Process (0) 2025.02.27 [Salesforce Knowledge] Agentforce (0) 2025.02.26 [Salesforce Knowledge] Einstein Trust Layer (0) 2025.02.25