5

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:

  1. The user edits more than one record from the list view
  2. My desired child records are inserted
  3. At least one of the records fails an edit due to a validation error
  4. 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)

  5. The operation repeats after removing the unsuccessfully updated records from the request

  6. The triggerRuns static variable assumes the operation has ocurred already, and does not execute my desired end function to insert the related records
  7. 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?

2 Answers 2

1

Check out the blog post I wrote, I hope this helps

http://www.oyecode.com/2012/11/controlling-recursive-triggers-how-to.html

and

http://www.oyecode.com/2012/11/apex-design-pattern-to-avoid-recursive.html

1
  • Thanks for the suggestions, but I don't see how this solves my issue. If the rollback Salesforce executes doesn't clear the static variables: then doesn't this just end up with the same result? Commented Jan 16, 2014 at 22:12
0

The inconsistency at the end is your main problem: the InventoryAdjustment__c value remains at 'Committed' because it does not get rolled back.

You could get around this by having two picklists:

  • InventoryAdjustment__c the main one, which the user can not edit
  • Change_InventoryAdjustment_To__c is a copy which the user can edit. Most of the time it is blank

For this to work the user needs to understand that 'InventoryAdjustment__c' represents the true state of things, but 'Change_InventoryAdjustment_To__c' is the field they edit when they want to make a change.

So then your triggers and workflow rules etc are kicked off by the change to Change_InventoryAdjustment_To__c. Then at the very end of everything you change InventoryAdjustment__c to 'Committed' only if everything else is worked. You could probably clear 'Change_InventoryAdjustment_To__c' back to blank at the end as well.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .