Skip to content

Instantly share code, notes, and snippets.

@omniphx
Last active March 15, 2018 15:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save omniphx/132f19a034cbb1e1d726917ffeb9be72 to your computer and use it in GitHub Desktop.
Save omniphx/132f19a034cbb1e1d726917ffeb9be72 to your computer and use it in GitHub Desktop.
TriggerHandler with hashmap to prevent recursion
public virtual class TriggerHandler {
public static Boolean shouldRun = true;
public static Boolean shouldHandlersRun = true;
public TriggerContext context;
public static Boolean isFirstTime = true;
private Integer hashCode;
private Boolean isTriggerExecuting;
private static Map<Integer, Set<TriggerContext>> processedHashCodes = new Map<Integer, Set<TriggerContext>>();
private final List<String> OMITTED_FIELDS = new List<String>{'CompareName','CreatedById','CreatedDate','LastModifiedById','LastModifiedDate','SystemModstamp'};
public static void disable() {
TriggerHandler.shouldRun = false;
}
public static void enable() {
TriggerHandler.shouldRun = true;
}
public enum TriggerContext {
BEFORE_INSERT, BEFORE_UPDATE, BEFORE_DELETE,
AFTER_INSERT, AFTER_UPDATE, AFTER_DELETE, AFTER_UNDELETE
}
protected TriggerHandler() {
this.setHashCode();
this.isTriggerExecuting = Trigger.isExecuting;
if(!this.isTriggerExecuting) return;
else if(Trigger.isBefore && Trigger.isInsert) this.context = TriggerContext.BEFORE_INSERT;
else if(Trigger.isBefore && Trigger.isUpdate) this.context = TriggerContext.BEFORE_UPDATE;
else if(Trigger.isBefore && Trigger.isDelete) this.context = TriggerContext.BEFORE_DELETE;
else if(Trigger.isAfter && Trigger.isInsert) this.context = TriggerContext.AFTER_INSERT;
else if(Trigger.isAfter && Trigger.isUpdate) this.context = TriggerContext.AFTER_UPDATE;
else if(Trigger.isAfter && Trigger.isDelete) this.context = TriggerContext.AFTER_DELETE;
else if(Trigger.isAfter && Trigger.isUndelete) this.context = TriggerContext.AFTER_UNDELETE;
}
public void execute() {
if(!TriggerHandler.shouldRun) return;
if(this.recordsProccessed()) return;
this.executeHandlers();
}
public void executeHandlers() {
if(this.context == TriggerContext.BEFORE_INSERT) this.beforeInsert(Trigger.new);
else if(this.context == TriggerContext.BEFORE_UPDATE) this.beforeUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.BEFORE_DELETE) this.beforeDelete(Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_INSERT) this.afterInsert(Trigger.new, Trigger.newMap);
else if(this.context == TriggerContext.AFTER_UPDATE) this.afterUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_UNDELETE) this.afterUndelete(Trigger.new, Trigger.newMap);
}
/**
* This method is a safeguard that checks to see if we have recursion problems and stops if we do
* It allows each context to occur once for a given hash code
*/
private Boolean recordsProccessed() {
System.debug('hash code');
System.debug(this.hashCode);
System.debug(TriggerHandler.processedHashCodes);
if(this.context == TriggerContext.BEFORE_INSERT) {
// BEFORE_INSERT doesn't have record IDs yet, so the hash here will never match the other hashes
// Since Salesforce makes it impossible to recursively run "insert record", we can let the platform handle it
return false;
} else if(!TriggerHandler.processedHashCodes.containsKey(this.hashCode)) {
TriggerHandler.processedHashCodes.put(this.hashCode, new Set<TriggerContext>{this.context});
return false;
} else if(!TriggerHandler.processedHashCodes.get(this.hashCode).contains(this.context)) {
TriggerHandler.processedHashCodes.get(this.hashCode).add(this.context);
return false;
} else {
return true;
}
}
private void setHashCode() {
List<SObject> records = Trigger.new != null ? Trigger.new : Trigger.old;
List<String> parsedRecordsJson = new List<String>();
for(SObject record : records) {
// Some audit fields can cause the hash code to change even when the record itself has not
// To get a consistent hash code, we deserialize into JSON, remove the problematic fields, then get the hash code
// If that sounds complicated, it is, but let's accept it and move on with our lives!
Map<String, Object> parsedRecordMap = (Map<String, Object>)JSON.deserializeUntyped(JSON.serialize(record));
this.removeAll(parsedRecordMap);
// Since we're using an untyped object (map) & JSON string to generate the hash code, we need to sort the fields
Map<String, Object> sortedRecordMap = this.sortRecordMap(parsedRecordMap);
parsedRecordsJson.add(JSON.serialize(sortedRecordMap));
}
this.hashCode = parsedRecordsJson.hashCode();
}
public void removeAll(Map<String,Object> mapping) {
Integer size = this.OMITTED_FIELDS.size();
for(Integer i = 0; i < size; i++) {
mapping.remove(this.OMITTED_FIELDS[i]);
}
}
private Map<String, Object> sortRecordMap(Map<String, Object> recordMap) {
Map<String, Object> sortedRecordMap = new Map<String, Object>();
List<String> sortedKeySet = new List<String>(recordMap.keySet());
sortedKeySet.sort();
for(String key : sortedKeySet) sortedRecordMap.put(key, recordMap.get(key));
return sortedRecordMap;
}
protected virtual void beforeInsert(List<SObject> newRecords) {}
protected virtual void beforeUpdate(List<SObject> updatedRecords, Map<Id, SObject> updatedRecordsMap, List<SObject> oldRecords, Map<Id, SObject> oldRecordsMap) {}
protected virtual void beforeDelete(List<SObject> deletedRecords, Map<Id, SObject> deletedRecordsMap) {}
protected virtual void afterInsert(List<SObject> newRecords, Map<Id, SObject> newRecordsMap) {}
protected virtual void afterUpdate(List<SObject> updatedRecords, Map<Id, SObject> updatedRecordsMap, List<SObject> oldRecords, Map<Id, SObject> oldRecordsMap) {}
protected virtual void afterDelete(List<SObject> deletedRecords, Map<Id, SObject> deletedRecordsMap) {}
protected virtual void afterUndelete(List<SObject> undeletedRecords, Map<Id, SObject> undeletedRecordsMap) {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment