Last active
March 15, 2018 15:46
-
-
Save omniphx/132f19a034cbb1e1d726917ffeb9be72 to your computer and use it in GitHub Desktop.
TriggerHandler with hashmap to prevent recursion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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