46

I need to be able to detect that the trigger execution is happening because of a workflow field update. There is a suggestion in the cookbook:

Controlling Recursive Triggers

But this solution is not properly bulkified. If my batch of records is split up (200, 100, 50 whatever the server decides) the 2nd to nth batches are not processed correctly because the flag has already been set.

The second part of the problem is: This is part of a managed package. I have no control of the customer's workflow rules, supporting and configuring a flag/last changed field on a record is not realistic, our company doesn't have the support resources to be walking each customer through the configuration.

Has anyone been able to solve this problem for large batches of records?

Edit (In Response to Calebs Answer)

Separate trigger batches do not run in separate contexts

trigger AccountTriggerContext on Account (before insert) {
    System.debug('counter='+TriggerContextCounter.getCounter());
}

.

global without sharing class TriggerContextCounter {

    private static Integer counter = 0;
    
    public static Integer getCounter() {
        Integer value = counter;
        counter++;
        return value;
    }

    private static testMethod void testTriggerContexts() {
        List<Account> accounts = new List<Account>();
        for(Integer i = 0; i < 250; i++) {
            accounts.add(new Account(Name='testTriggerContexts: ' + i));
        }
        insert accounts;
    }
}

Unit test debug messages

USER_DEBUG|[2]|DEBUG|counter=0

USER_DEBUG|[2]|DEBUG|counter=1

Both counters should have been 0 if the batches were running in separate contexts

Edit Further explanation of background for @Ralph

This is a generalized explanation, but when certain conditions are met, some date fields on other objects are updated. The trigger runs and updates the dates.

If a workflow rule causes a field update on the object, the same trigger is run again and the dates on the other objects are updated for a second time. And because this is an increment operation the dates on the other objects are off by a factor of 2.

4
  • 1
    Here's a thread on the SF boards from years ago discussing this same basic issue. Response was: intended behaviour. The recommendation was to reduce the batch size or code with this functionality in mind. It was noted this was not clearly documented and this would be recitified. Commented Jan 23, 2013 at 17:01
  • 1
    Daniel, could you give a little background about what you're trying to prevent in the second executions? There are a bunch of options out there, but they vary depending on your use case. Commented Feb 19, 2013 at 16:03
  • 1
    @Ralph, I have added a bit more background to my answer for you Commented Feb 19, 2013 at 21:25
  • 1
    @DanielBlackhall added an answer that might help. Still might not match your use case exactly, but the idea of caching changes for later inspection may prove useful Commented Feb 19, 2013 at 22:21

4 Answers 4

13

I have a trigger that creates new child records when a field's value is changed and ended up with a similar situation where I ended up with two child records per field change when workflow was run. In the end I created a Map to record what records were process (by Id, key of the map) and the new value of the field when I created the child record. I then checked this map to make sure I wasn't re-processing the same field update.

It's a lot of extra complexity and code and my use case is simpler than a lot. I'm not quite sure why salesforce things running triggers twice for the same transaction when workflow is involved is a good thing.

7
  • 1
    Looks like I'm not getting any other answers :) Thanks for the response, keeping a Map or Set of Ids appears to be the only way. Commented Aug 7, 2012 at 7:01
  • 2
    Yeah, I was rather surprised that when they say "runs triggers again" they mean literally it passes the same Trigger.new and Trigger.old values in a 2nd time. I would have asked them to fix it as a bug but at this point I'm sure many things (unknowingly) rely on this. Commented Aug 7, 2012 at 15:13
  • 2
    Yes I was almost going to do a follow up question about this. I spent hours debugging to discover that the new value of the field update is not available to the second execution of the trigger. What is the point of running the trigger a second time for? Commented Aug 7, 2012 at 22:48
  • 2
    I have absolutely no idea. I'm going to ask some people in salesforce R&D that next time I get a chance and see if there's a sane reason for it. Commented Aug 7, 2012 at 22:53
  • 2
    This approach will work 99% of the time, however it'll break if a the field changes and then a workflow changes the field again ... If you want to handle that case you'll need to use a trigger old map override Commented Jan 21, 2014 at 3:19
7

The example in the cookbook should work regardless of bulk size. Create a helper class, here's what I typically use:

global class SingleExecution {

private static boolean blnAlreadyDone = false;

public static boolean hasAlreadyDone(){ 
    return blnAlreadyDone;
}

public static void setAlreadyDone() {
    blnAlreadyDone = true;  
}

public static void forceResetAlreadyDone() {
    blnAlreadyDone = false; 
}

static testMethod void testSingleExecution() {
    //Hasn't already run
    System.assertEquals(false,SingleExecution.hasAlreadyDone());

    SingleExecution.setAlreadyDone();
    //Has just been run
    System.assertEquals(true,SingleExecution.hasAlreadyDone());

    SingleExecution.forceResetAlreadyDone();
    //Has just been reset
    System.assertEquals(false,SingleExecution.hasAlreadyDone());
}   
}

then in your trigger you can do

trigger AccountTrigger on Account (before update) {
    if(SingleExecution.hasAlreadyDone()) return;
    //Else
    SingleExecution.setAlreadyDone();
    //Do your stuff
}

Even though you may data load say 350 accounts which would cause this trigger to fire twice...once for the fist 200 records and then again for the next 150 records each time the trigger fires it has it's own context and so the SingleExecution flag is re-set in each of those separate contexts.

You can also get more fancy with your SingleExecution and allow it to be used across various triggers/classes say for preventing @future from being called more than once because instead of a single boolean, it lets you pass a string that with the name of your class. Because of context you typically don't need this, however if you had two triggers that might call each other and you want them BOTH to fire once, then you'd need to differentiate between them somehow.

Hope this helps...

global without sharing class SingleExecution {

private static Map<String,Boolean> singletonMap;

global static Boolean hasAlreadyExecuted(String ClassNameOrExecutionName){  
    if(singletonMap != null) {
        Boolean alreadyExecuted = singletonMap.get(ClassNameOrExecutionName);
        if(alreadyExecuted != null) {
            return alreadyExecuted;
        }           
    }
    //By default return false
    return false;
}

global static void setAlreadyExecuted(String ClassNameOrExecutionName) {
    if(singletonMap == null) {
        singletonMap = new Map<String,Boolean>();
    }
    singletonMap.put(ClassNameOrExecutionName,true);
}

static testMethod void testSingleExecution() {
    //Hasn't already run
    System.assertEquals(false,SingleExecution.hasAlreadyExecuted('testSingleExecution'));

    SingleExecution.setAlreadyExecuted('testSingleExecution');
    //Has just been run
    System.assertEquals(true,SingleExecution.hasAlreadyExecuted('testSingleExecution'));
}

}
5
  • 3
    I have just tested this and you're incorrect mutliple batches do not run in separate contexts. Commented Aug 3, 2012 at 1:38
  • 1
    The Import Wizard is especially bad about running multiple batches in the same apex context, or at least it has been bad about that in the past. Commented Aug 3, 2012 at 3:16
  • 1
    Sorry @DanielBlackhall not sure what to say, I use this design pattern regularly and it works whenever I use Apex Data Loader...maybe we're running on different API versions.
    – caleb
    Commented Aug 15, 2012 at 13:35
  • 6
    It makes sense that it might work using the data loader because the loader is only sending batches of 200 per api request (which could be a new context each time). but when saving a large number of records in an apex controller, the batches are processed in the same context Commented Aug 16, 2012 at 0:44
  • 2
    This approach can also fail if a record is altered by workflow and is supposed to be caught by a trigger. E.g. you're trigger fires every time the close date changes, if the close date changes via workflow the triggers will already have fired once, and you'll miss catching the close date change. Commented Jan 21, 2014 at 3:21
7

While I saw the solution described (store IDs in a map or set), I didn't see any code. In the event you want something that you can cut and paste. Disclaimer: I have tested this against single record updates and it works fine (prevents my logic from running on the second trigger execution). I have not tested it against batch updates.

public class TriggerRunOnce {
    private static Set <Id> idSet = new Set <Id>();

    // has this Id been processed? 
    public static boolean isAlreadyDone(Id objectId) {
        return (idSet.contains(objectId));
    }

    // set that this Id has been processed.
    public static void setAlreadyDone(Id objectId) {
        idSet.add(objectId);
    }

    // empty set if we need to for some reason. 
    public static void resetAlreadyDone() {
        idSet.clear();
    }

}

and your trigger:

if (!TriggerRunOnce.isAlreadyDone(Obj.Id)) {
    // do your processing
    TriggerRunOnce.setAlreadyDone(Obj.Id);
}
3
  • 1
    Any reason why the class is global? Commented Feb 18, 2013 at 23:24
  • 1
    No, was just jumping the gun a bit. Have changed it to public. I'm going to include this in a managed package and want all our customers to have access, so will need to make the class and all methods global when I do that. Commented Feb 18, 2013 at 23:43
  • 2
    This will work 99% of the time, but will fail if a workflow update should send the object through the trigger logic a second time. Imagine a trigger wants to track changes to approval status, it gets changed to "approved", and then a workflow fires to change it to "active" if the start date is today. The trigger will ignore the second status change since it sees the object as "already done". Commented Jan 21, 2014 at 3:23
5

If you're only looking to ensure you process the same object once, i.e. create a child object when a record reaches a certain status, tracking the set of ids processed will be sufficient. However, if you're tracking a change in a field, which in theory could happen twice if the workflow has updated that field you'll want to work around some unexpected behavior with trigger.old.

You might expect that after the first pass through the trigger trigger.new would then become trigger.old, but it'll actually contain the same values as started with the trigger. Here's an example for tracking a status change through multiple trigger batches.

/* 
Track how many times the stage has changed for an opportunity
*/
public class TriggerLogic {

  private static Map<Id, Opportunity> oldMapFix = new Map<Id, Opportunity>();
  private static Map<Id, Opportunity> oldMap;
  private static List<Opportunity> newList;

  public TriggerLogic(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
    this.newList = newList;
    this.oldMap = oldMap;
  }

  // NB: assuming we're called from a before update trigger
  public static countStatusChanges() {

    for(Opportunity newOppty : newList) {

      Opportunity oldOppty = (oldMapFix.contains(newOppty.id))
        ? oldMapFix.get(newOppty.id)
        : oldMap.get(newOppty.id);

      if(newOppty.stageName != oldOppty.stageName) {
        newOppty.stage_change_count__c = (newOppty.stage_change_count__c == null)
          ? 1 : newOppty.stage_change_count__c + 1;

        // stash correct oppty with new status so we don't
        // double count a status change if we have multiple 
        // trigger executions
        oldMapFix.put(newOppty.id, newOppty);

      }
    }
  }
}

This captures the following scenarios correctly. First

  1. Opportunity stage changed
  2. Triggers fire, stage_count__c incremented
  3. Workflows fire and field updates (not to status) triggers second execution
  4. Trigger can correctly recognize stage has not changed again

And second

  1. Opportunity stage changed
  2. Trigger first, stage_count__c incremented
  3. Workflows fire and field updates change the status again
  4. Trigger correctly increments stage_count__c a second time

You must log in to answer this question.

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