11public abstract class TriggerHandler {
22
3- public TriggerContext context ; // the current context of the trigger
3+ private static Map <Integer , Set <TriggerContext >> hashCodesForProcessedRecords = new Map <Integer , Set <TriggerContext >>();
4+
45 public enum TriggerContext {
56 BEFORE_INSERT , BEFORE_UPDATE , BEFORE_DELETE ,
67 AFTER_INSERT , AFTER_UPDATE , AFTER_DELETE , AFTER_UNDELETE
78 }
9+ public TriggerContext context ; // The current context of the trigger
810
9- private Integer hashCode ; // the hash code for the current records
10- private Boolean isTriggerExecuting ; // the current context of the trigger
11+ private Integer hashCode ; // The hash code for the current records
12+ private Boolean isTriggerExecuting ; // Checks if the code was called by a trigger
1113
1214 protected TriggerHandler () {
1315 this .setTriggerContext ();
1416 this .validateTriggerContext ();
17+ this .setHashCode ();
1518 }
1619
17- public void run () {
18- if (this .context == TriggerContext .BEFORE_INSERT ) this .beforeInsert (Trigger .new );
20+ public void execute () {
21+ String sobjectType = Trigger .new == null ? Trigger .old .getSObjectType () : Trigger .new .getSObjectType ();
22+ System .debug (' Starting execute method for: ' + sobjectType );
23+ System .debug (' Hash codes already processed: ' + TriggerHandler .hashCodesForProcessedRecords );
24+ System .debug (' Hash code for current records: ' + this .hashCode );
25+ System .debug (' Trigger context for current records: ' + this .context );
26+ System .debug (' Number of current records: ' + Trigger .size );
27+
28+ if (this .haveRecordsAlreadyBeenProcessed ()) {
29+ System .debug (' Records already processed for this context, skipping' );
30+ return ;
31+ } else System .debug (' Records have not been processed for this context, continuing' );
32+
33+ if (this .context == TriggerContext .BEFORE_INSERT ) this .beforeInsert (Trigger .new );
1934 else if (this .context == TriggerContext .BEFORE_UPDATE ) this .beforeUpdate (Trigger .new , Trigger .newMap , Trigger .old , Trigger .oldMap );
2035 else if (this .context == TriggerContext .BEFORE_DELETE ) this .beforeDelete (Trigger .old , Trigger .oldMap );
2136 else if (this .context == TriggerContext .AFTER_INSERT ) this .afterInsert (Trigger .new , Trigger .newMap );
2237 else if (this .context == TriggerContext .AFTER_UPDATE ) this .afterUpdate (Trigger .new , Trigger .newMap , Trigger .old , Trigger .oldMap );
2338 else if (this .context == TriggerContext .AFTER_DELETE ) this .afterDelete (Trigger .old , Trigger .oldMap );
2439 else if (this .context == TriggerContext .AFTER_UNDELETE ) this .afterUndelete (Trigger .new , Trigger .newMap );
40+
41+ this .runDmlForRelatedRecords ();
2542 }
2643
27- protected virtual void beforeInsert (List <SObject > newRecords ) {}
28- protected virtual void beforeUpdate (List <SObject > updatedRecords , Map <Id , SObject > updatedRecordsMap , List <SObject > oldRecords , Map <Id , SObject > oldRecordsMap ) {}
29- protected virtual void beforeDelete (List <SObject > deletedRecords , Map <Id , SObject > deletedRecordsMap ) {}
30- protected virtual void afterInsert (List <SObject > newRecords , Map <Id , SObject > newRecordsMap ) {}
31- protected virtual void afterUpdate (List <SObject > updatedRecords , Map <Id , SObject > updatedRecordsMap , List <SObject > oldRecords , Map <Id , SObject > oldRecordsMap ) {}
32- protected virtual void afterDelete (List <SObject > deletedRecords , Map <Id , SObject > deletedRecordsMap ) {}
33- protected virtual void afterUndelete (List <SObject > undeletedRecords , Map <Id , SObject > undeletedRecordsMap ) {}
44+ protected virtual void beforeInsert (List < SObject > newRecordList ) {}
45+ protected virtual void beforeUpdate (List < SObject > updatedRecordList , Map < Id , SObject > updatedRecordMap , List < SObject > oldRecordList , Map < Id , SObject > oldRecordMap ) {}
46+ protected virtual void beforeDelete (List < SObject > deletedRecordList , Map < Id , SObject > deletedRecordMap ) {}
47+ protected virtual void afterInsert (List < SObject > newRecordList , Map < Id , SObject > newRecordMap ) {}
48+ protected virtual void afterUpdate (List < SObject > updatedRecordList , Map < Id , SObject > updatedRecordMap , List < SObject > oldRecordList , Map < Id , SObject > oldRecordMap ) {}
49+ protected virtual void afterDelete (List < SObject > deletedRecordList , Map < Id , SObject > deletedRecordMap ) {}
50+ protected virtual void afterUndelete (List < SObject > undeletedRecordList , Map < Id , SObject > undeletedRecordMap ) {}
3451
3552 private void setTriggerContext () {
3653 this .isTriggerExecuting = Trigger .isExecuting ;
@@ -50,4 +67,48 @@ public abstract class TriggerHandler {
5067 if (! this .isTriggerExecuting || this .context == null ) throw new Exceptions .TriggerHandlerException (errorMessage );
5168 }
5269
70+ private void setHashCode () {
71+ List < SObject > recordList = Trigger .new != null ? Trigger .new : Trigger .old ;
72+ List < String > parsedRecordsJson = new List <String >();
73+ for (SObject record : recordList ) {
74+ // Some fields can cause the hash code to change even when the record itself has not
75+ // To get a consistent hash code, we deserialize into JSON, remove the problematic fields, then get the hash code
76+ Map < String , Object > parsedRecordMap = (Map <String , Object >)JSON .deserializeUntyped (JSON .serialize (record ));
77+ parsedRecordMap .remove (' CompareName' );
78+ parsedRecordMap .remove (' CreatedById' );
79+ parsedRecordMap .remove (' CreatedDate' );
80+ parsedRecordMap .remove (' LastModifiedById' );
81+ parsedRecordMap .remove (' LastModifiedDate' );
82+ parsedRecordMap .remove (' SystemModstamp' );
83+
84+ // Since we're using an untyped object (map) & JSON string to generate the hash code, we need to sort the fields
85+ // Maps & sets aren't sortable, so we have to sort it ourselves
86+ Map < String , Object > sortedRecordMap = new Map <String , Object >();
87+ List < String > sortedKeyList = new List <String >(parsedRecordMap .keySet ());
88+ sortedKeyList .sort ();
89+ for (String key : sortedKeyList ) sortedRecordMap .put (key , parsedRecordMap .get (key ));
90+
91+ parsedRecordsJson .add (JSON .serialize (sortedRecordMap ));
92+ }
93+ this .hashCode = parsedRecordsJson .hashCode ();
94+ }
95+
96+ private Boolean haveRecordsAlreadyBeenProcessed () {
97+ // This method is a safeguard that checks to see if we have recursion problems and stops if we do
98+ // It allows each context to occur once for a given hash code
99+ if (this .context == TriggerContext .BEFORE_INSERT ) {
100+ // BEFORE_INSERT doesn't have record IDs yet, so the hash here will never match the other hashes
101+ // Since Salesforce makes it impossible to recursively run "insert record", we can let the platform handle it
102+ return false ;
103+ } else if (! TriggerHandler .hashCodesForProcessedRecords .containsKey (this .hashCode )) {
104+ TriggerHandler .hashCodesForProcessedRecords .put (this .hashCode , new Set <TriggerContext >{this .context });
105+ return false ;
106+ } else if (! TriggerHandler .hashCodesForProcessedRecords .get (this .hashCode ).contains (this .context )) {
107+ TriggerHandler .hashCodesForProcessedRecords .get (this .hashCode ).add (this .context );
108+ return false ;
109+ } else {
110+ return true ;
111+ }
112+ }
113+
53114}
0 commit comments