I have a pretty complicated issue with users mass editing record from the Enhanced list View and using static methods to control the flow of recursive triggers.
In case you are not familiar, due to the Salesforce Order of Execution: you may encounter circumstances where code is executed twice during coplex DML operations. In order to prevent unwanted functions being run, the generally accepted solution is to control these triggers with an external static variable that can be used to determine if code has already run. (See: Controlling Recursive Triggers on DeveloperForce Cookbook)
All of this is functioning as expected and desired in my application. However, once enabling "Mass Edit From List Views" I encountered an issue where my code got rolled back - and not subsequently re-executed.
A simple explanation of my process:
- When a picklist is changed from Pending to Committed: then some DML occurs to insert related records
- Due to the order of execution and recursion - a static "Trigger Helper" is in place to ensure that the operation only occurs once
The Trigger Helper
public with sharing class TriggerRuns{
/* A utility class to ensure that Triggers only run once during a transaction
* This is a known salesforce issue, and this is the suggested solution
*
* Call the isFirstRun method with a run type (eg: 'allocating quantities') and a record id
* and get back the boolean of whether or not the operation has run before
*/
private static map<string, set<id>> runs{
get {
if(runs == null) {
runs = new map<string,set<id>>();
}
return runs;
}
set;
}
public static boolean isFirstRun(string pRunType, id pId) {
string runType = pRunType.toUpperCase();
system.debug('UDBG:::triggerRuns.isFirstRun ENTERED with runType: ' + runType + ', id: ' + pId);
boolean b = true;
if(runs.get(runType) == null) {
runs.put(runType, new set<id>());
system.debug('UDBG:::triggerRuns.isFirstRun this is the first ever run for runType: ' + runType);
}
if(runs.get(runType).contains(pId)){
b = false;
system.debug('UDBG:::triggerRuns.isFirstRun FALSE: this is not the first run for runType: ' + runType + ', id: ' + pId);
} else {
runs.get(runType).add(pId);
system.debug('UDBG:::triggerRuns.isFirstRun TRUE: this is the first run for runType: ' + runType + ', id: ' + pId);
}
return b;
}
}
The trigger
trigger inventoryAdjustment_trg on InventoryAdjustment__c (after update) {
recordContext rc = new recordContext(trigger.new, trigger.old, trigger.newMap, trigger.oldMap, trigger.isInsert, trigger.isUpdate, trigger.isDelete, trigger.isUndelete, trigger.isBefore, trigger.isAfter);
inventoryAdjustment_ext e = new inventoryAdjustment_ext(rc);
if(rc.mode == recordContext.ModeType.AU) {
e.transact();
}
}
The method that is being called via the trigger - note that the transact method calls another method - this is inconsequential so I did not include it - but it simply inserts some records
public with sharing class InventoryAdjustment_ext extends inventoryAdjustment_ctrl{
public inventoryAdjustment_ext(recordContext c) {
this.context = c;
}
////////////// TRANSACT //////////////
//Transacting line items through the various stateChanges
// This should be called after update - it will automatically identify what line items are needed to transact
//Conditions under which we need to transact are as follows:
//1) We are statechanging:
// if we are COMPLETED
public void transact(){
list<InventoryAdjustment__c> INrecords = new list<InventoryAdjustment__c>();
list<InventoryAdjustment__c> OUTrecords = new list<InventoryAdjustment__c>();
map<stateChange,list<InventoryAdjustment__c>> scs = getStateChangeMap(
new set<stateChange>{stateChange.COMPLETED} //basically checks that the picklist has changed from 'Pending' to 'Complete'
);
for(InventoryAdjustment__c r : scs.get(stateChange.COMPLETED)){
if(triggerRuns.isFirstRun('ADJ_TRANSACTING',r.id)) {
if(r.Quantity__c > 0) {
INrecords.add(r);
} else if(r.Quantity__c < 0){
OUTrecords.add(r);
}
}
}
this.transact(INrecords, OUTrecords);
}
}
What goes wrong, and how it happens:
- The user edits more than one record from the list view
- My desired child records are inserted
- At least one of the records fails an edit due to a validation error
Salesforce executes a DB rollback operation for the failures (this is not controlled by us, it seems to be how they execute their batch updates in the mass edit functionality)
- As per the Transaction Control documentation salesforce does not revert static variables
The operation repeats after removing the unsuccessfully updated records from the request
- The triggerRuns static variable assumes the operation has ocurred already, and does not execute my desired end function to insert the related records
- The picklist value on the Parent InventoryAdjustment__c remains 'Committed' despite not actually running the code because the operation has been rolled back
So then, the user assumes the operation has ocurred (in a way it has) but the result is that it really did nothing.
How can I fix this?