1

When we can avoid recursion conditionally using oldMap and NewMap in trigger to restrict what change fires the triggers, why do you have to go for static approach?

1 Answer 1

7

We need to take measures to prevent recursion precisely because the approach you're suggesting (compare old and new trigger context records) doesn't work. Or, at the very least, it doesn't work in all situations.

First things first: Triggers fire when DML occurs. You cannot prevent a trigger from firing when you perform DML. You can prevent the code that the trigger executes from being run, but the trigger itself will always fire (assuming you've defined a trigger on the SObject).

For workflow field updates (and I imagine for after-save record triggered flows, flows, and process builder too, though I haven't experimented as thoroughly with those), the field update causes update triggers to be run again. However, the values stored in trigger.old and trigger.oldMap are not updated.

So it was fairly common for people to have triggers that checked old vs new like

trigger X on SObjectY (before update){
    for(SObjectY newRec :trigger.new){
        SObjectY oldRec = trigger.oldMap.get(newRec.Id);

        if(oldRec.Field__c != newRec.Field__c){
            newRec.Other_Field__c += 1;
        }
    }
}

but since field update actions don't cause trigger.old or trigger.oldMap data to be updated, the second run of the trigger will still satisfy oldRec.Field__c != newRec.Field__c and thus increment Other_Field__c again.

While checking old vs new can work, in limited circumstances, we generally can't rely on those circumstances being in place all the time. Static variables (declared in an Apex class, not a trigger) on the other hand are reliable (until you start messing around with allOrNone = false in DML). Once set, static variables retain their value for the rest of the transaction. That includes things like a workflow field update causing triggers to run again, or you performing DML inside of your trigger (that causes another trigger to fire).

Just don't use something like static boolean firstRun unless you're putting a recursion guard in place for a specific DML call. Otherwise, you'll cause your trigger(s) to only run for the first chunk of 200 records. At the very least, you'd want a static Set<Id> alreadyProcessed so you can check to see if you've encountered a given Id before.

The simplest recursion guard (using the static boolean) should look like this

// Apex class
public class RecursionGuard{
    public static Boolean isRunning = false;
}

// Trigger
trigger X on SObjectY (after update){
    if(RecursionGuard.isRunning){ return; }

    List<SObjectY> othersToUpdate = new List<SObjectY>();
    for(SObjectY rec :trigger.new){
        othersToUpdate.add(new SObjectY(
            Id = rec.ParentId
        ));
    }

    // By setting the guard just before the DML, and unsetting it
    //   just after, we accomplish 2 things:
    // 1) This trigger is not run again for the "othersToUpdate" records
    //      (preventing infinite recursion)
    // 2) This trigger will still run for the next chunk of 200 records
    RecursionGuard.isRunning = true;
    update othersToUpdate;
    RecursionGuard.isRunning = false;
}

As a final note, guarding against recursion is something I think that your trigger framework should be handling (even if it's as simple as just allowing you to temporarily disable your triggers, programatically), and you should be using a trigger framework. Triggers should contain effectively no logic, they should just call into your trigger framework. I put logic in my example triggers to keep things simple to digest.

2
  • 1
    Even with the Set<Id> approach, it is valid to potentially encounter the same Id twice in a valid context (e.g. Database.update(records, false) and 1 or more records have an error). The last bit of code is what I recommend, because it is the only one that guarantees that actual recursion is blocked without blocking various retry mechanisms.
    – sfdcfox
    Jun 19, 2022 at 14:15
  • 1
    N.B. Triggers will re-fire on DML from Process Builder actions and Flow Create/Update/Delete elements.
    – cropredy
    Jun 19, 2022 at 18:10

You must log in to answer this question.

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