Skip to content
This repository was archived by the owner on Sep 27, 2023. It is now read-only.

Commit fedfd48

Browse files
authored
Recursion detection & prevention (#3)
* Added first version of recursion detection & prevention * Updated naming convention used for collections
1 parent 50feb51 commit fedfd48

1 file changed

Lines changed: 73 additions & 12 deletions

File tree

src/classes/TriggerHandler.cls

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,53 @@
11
public 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

Comments
 (0)