1

I have a trigger framework based on the work by Hari Krishnan: An architecture framework to handle triggers on the Force.com platform, however I have functionality on After Update in wich a trigger dispatcher on Accounts creates an instance of an After Update handler for Accounts where a Helper class is called to process the newMap to be processed. Basically the logic here is:

IF NOT a batch process the following:

  1. IF Account.PersonEmail is NOT NULL and Account.Phoenix_People_Id__pc is NULL THEN call an Async Batch Process to make a callout to an endpoint and populate the Account.Phoenix_People_Id__pc on the specified account records.

However this results in the batch process being executed twice! My thinking here is that the first time the update occurs and the condition is met to call the batch process, then the After Update for the Batch shouldnt make another call (I hope this makes sense).

Below is the classes involved in the process

Account Trigger

trigger AccountTrigger on Account (after insert, after update) {

    TriggerFactory.createTriggerDispatcher(Account.sObjectType);

}

TriggerFactory

/**
* This factory creates the correct dispatcher and dispatches the trigger event(s) to the appropriate event handler(s). 
* The dispatchers are automatically created using the Type API, hence dispatcher registration is not required for each 
* dispatchers.
*
* @author   Nelson Chisoko
* @created  2019/07/30
*/

public with sharing class TriggerFactory {

    /** 
    * Creates the appropriate dispatcher and dispatches the trigger event to the dispatcher's event handler method.
    * 
    * @param    Schema.sObjectType  Object type to process (SObject.sObjectType)
    */
    public static void createTriggerDispatcher(Schema.sObjectType soType) {

        ITriggerDispatcher dispatcher = getTriggerDispatcher(soType);

        if (dispatcher == null)
            throw new TriggerException('No Trigger dispatcher registered for Object Type: ' + soType);

        execute(dispatcher);

    }

    /** 
    * Dispatches to the dispatcher's event handlers.
    *
    * @param    ITriggerDispatcher  A Trigger dispatcher that dispatches to the appropriate handlers
    */ 
    private static void execute(ITriggerDispatcher dispatcher) {

        TriggerParameters tp = new TriggerParameters(
            Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap, 
            Trigger.isBefore, Trigger.isAfter, Trigger.isDelete, 
            Trigger.isInsert, Trigger.isUpdate, Trigger.isUnDelete, Trigger.isExecuting
        );

        // Handle before trigger events
        if (Trigger.isBefore) {

            dispatcher.bulkBefore();             

        } else {    // Handle after trigger events

            dispatcher.bulkAfter();

            if (Trigger.isInsert)
                dispatcher.afterInsert(tp);

            else if (Trigger.isUpdate)
                dispatcher.afterUpdate(tp);

        }

        dispatcher.andFinally();

    } 

    /** 
    * Gets the appropriate dispatcher based on the SObject. It constructs the instance of the dispatcher dynamically 
    * using the Type API. The name of the dispatcher has to follow this format:
    *       <ObjectName>TriggerDispatcher. For e.g. for the Feedback__c object, the dispatcher has to be named
    *       as FeedbackTriggerDispatcher.
    *
    * @param    Schema.sObjectType  Object type to create the dispatcher
    *
    * @return   ITriggerDispatcher  A trigger dispatcher  if one exists or null.
    */

    private static ITriggerDispatcher getTriggerDispatcher(Schema.sObjectType soType) {

        String originalTypeName = soType.getDescribe().getName();
        String dispatcherTypeName = null;

        if (originalTypeName.toLowerCase().endsWith('__c')) {

            Integer index = originalTypeName.toLowerCase().indexOf('__c');
            dispatcherTypeName = originalTypeName.substring(0, index) + 'TriggerDispatcher';

        } else dispatcherTypeName = originalTypeName + 'TriggerDispatcher';

        Type obType = Type.forName(dispatcherTypeName);
        ITriggerDispatcher dispatcher = (obType == null) ? null : (ITriggerDispatcher)obType.newInstance();

        return dispatcher;

    }

}

AccountTriggerDispatcher

/**
* This class extends the TriggerDispatcherBase to provide the dispatching functionality for the trigger actions 
* on the Account object. The event handlers support allowing and preventing actions for reentrant scenarios. 
* This is controlled by the flag isBeforeXxxxx and isAfterXxxxx member variables. These variables need to be set
* to true before invoking the handlers and set to false after the invocation of the handlers. Resetting is MUST
* as otherwise unit tests MAY fail. The actual actions should be placed in the handlers (in a separate class).
*
* @author   Nelson Chisoko
* @created  2019/07/31
*/
public class AccountTriggerDispatcher extends TriggerDispatcherBase {

    private static Boolean isAfterInsertProcessing = false;
    private static Boolean isAfterUpdateProcessing = false;

    /** 
    * Called by the trigger framework to carry out the actions after the record(s) are created. If there is an
    * existing call running on the same context, the rentrant call will utilize the handler that was created
    * in the original call.
    *
    * @param    TriggerParameters   Contains the trigger parameters which includes the record(s) that got created.
    */
    public virtual override void afterInsert(TriggerParameters tp) {

        if (!isAfterInsertProcessing) {

            isAfterInsertProcessing = true;

            execute(new AccountAfterInsertTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterInsert);

            isAfterInsertProcessing = false;

        } else execute(null, tp, TriggerParameters.TriggerEvent.afterInsert);

    }

    /** 
    * Called by the trigger framework to carry out the actions after the records are updated. If there is an
    * existing call running on the same context, the rentrant call will utilize the handler that was created
    * in the original call.
    *
    * @param    TriggerParameters   Contains the trigger parameters which includes the record(s) that got updated.
    */  
    public virtual override void afterUpdate(TriggerParameters tp) {

        if (!isAfterUpdateProcessing) {

            isAfterUpdateProcessing = true;

            execute(new AccountAfterUpdateTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterUpdate);

            isAfterUpdateProcessing = false;

        } else execute(null, tp, TriggerParameters.TriggerEvent.afterUpdate);

    }

}

TriggerDispatcherBase

/**
* This class implements the ITriggerDispatcher and acts as an adapter to avoid implementing all the 
* ITriggerDispatcher methods.
*
* @author   Nelson Chisoko
* @created  2019/07/30
*/

public virtual class TriggerDispatcherBase implements ITriggerDispatcher { 

    private static ITriggerHandler afterInserthandler;
    private static ITriggerHandler afterUpdatehandler;

    /** 
    * This method is called prior to execution of a before trigger event. If you want  to load any 
    * lookup data or cache the data, then this is the place that you need to put your code. 
    */
    public virtual void bulkBefore() {}

    /** 
    * This method is called prior to execution of an after trigger event. 
    */
    public virtual void bulkAfter() {}

    /** 
    * This method is called for records inserted during an AFTER trigger. Always put field validation 
    * in the 'After' methods in case another trigger has modified any values. The record is 'read only' 
    * at this point.
    */
    public virtual void afterInsert(TriggerParameters tp) {} 

    /** 
    * This method is called iteratively for each record updated during an AFTER trigger.
    */
    public virtual void afterUpdate(TriggerParameters tp) {}

    public virtual void andFinally() {}

    /** 
    * Called by the event handlers. If this is the first call in the context, then this method will create a new 
    * instance of the appropriate handler and execute the mainEntry method. If there is an existing call runing 
    * on the same context, then this method will use the existing handler instance created by the original call
    * and execute the inProgressEntry method.
    *
    * @param    ITriggerHandler                 The trigger handler instance. The dispatcher need to pass an instance of the trigger handler, such
    *                                           as AccountAfterInsertTriggerHandler if this is the first call in a given context. If it is retry,
    *                                           then the dispatcher will need to pass null.
    * @param    TriggerParameters               The trigger parameters passed by the framework.
    * @param    TriggerParameters.TriggerEvent  The trigger event.
    */
    protected void execute(ITriggerHandler handlerInstance, TriggerParameters tp, TriggerParameters.TriggerEvent tEvent) {

        if (handlerInstance != null) {

            if (tEvent == TriggerParameters.TriggerEvent.afterInsert)
                afterInsertHandler = handlerInstance;

            if (tEvent == TriggerParameters.TriggerEvent.afterUpdate)
                afterUpdateHandler = handlerInstance;

            handlerInstance.mainEntry(tp);

        }

    }

}

ITriggerDispatcher

/**
* The interface for the trigger dispatching architecture.
*
* @author   Nelson Chisoko
* @created  2019/07/30
*/

public interface ITriggerDispatcher {
    /**
    * Called by the trigger framework to carry out the actions before the bulk operations.
    */  
    void bulkBefore();

    /**
    * Called by the trigger framework to carry out the actions after the bulk operations.
    */
    void bulkAfter();

    /**
    * Called by the trigger framework to carry out the actions after completing the bulk operations.
    */
    void andFinally();

    /** 
    * Called by the trigger framework to carry out the actions after the records are inserted.
    *
    * @param    TriggerParameters   Contains the trigger parameters which includes the records that got inserted.
    */
    void afterInsert(TriggerParameters tp);

    /** 
    * Called by the trigger framework to carry out the actions before the records are updated.
    *
    * @param    TriggerParameters   Contains the trigger parameters which includes the records that got updated.
    */
    void afterUpdate(TriggerParameters tp);

}

AccountAfterUpdateTriggerHandler

/**
* After Update Trigger handler class for Accounts 
*
* @author   Nelson Chisoko
* @created  2019/07/31
*/
public class AccountAfterUpdateTriggerHandler extends TriggerHandlerBase {

    public override void mainEntry(TriggerParameters tp) {

        AccountHelper.updateRetrievePhoenixPersonDetails((Map<Id, Account>)tp.newMap);

    }

}

AccountHelper

/**
* Helper class for Account trigger handlers
*
* @author   Nelson Chisoko
* @created  2019/07/31
*/
public class AccountHelper {

    public static Id updateRetrievePhoenixPersonDetails(Map<Id, Account> accountsMap) {

        Set<Id> idsToProcess = new Set<Id>();
        Id batchProcessId;

        if (!System.isBatch()) {

            for (Account a : accountsMap.values()) {

                if (a.PersonEmail != null && a.Phoenix_People_Id__pc == null) {

                    idsToProcess.add(a.Id);

                }

            }

            if (!idsToProcess.isEmpty()) {

                batchProcessId = AsyncApexFramework.submitBatch(new GetPhoenixPersonDetailsBatch(idsToProcess), 100, null, false);
                AsyncApexFramework.flush();

            }

        }

        return batchProcessId;

    }

}

I know its a lot of code, but I could use some assistance here

4
  • 2
    Without looking through your code, my immediate first check would be to see if there are any process builder flows or similar that could be causing the immediate chaining of a second update to the record.
    – Phil W
    Aug 13, 2019 at 9:36
  • Thanks @PhilW It seems there are a few processes already setup, do you recommend I move the processes into the trigger in order to manage or should I perhaps have an indicator on the record to say that a job has been queued for it or not? Aug 13, 2019 at 10:11
  • 1
    Try turning these off to see if that is the cause for these additional executions. Then you can look at ways to deal with these repeated updates. If these do cause it, anyone adding more later would re-introduce this issue. You probably, therefore, want to consider how to modify your code to detect when NOT to perform this processing again on an update.
    – Phil W
    Aug 13, 2019 at 10:59
  • 1
    Thanks @PhilW so it turns out it was the process builder processes that were causing the multiple executions. I simply added a flag on the target records which is updated the moment they are added to the batch and that worked perfectly. Thank you so much! Aug 13, 2019 at 13:52

2 Answers 2

1

So it turns out Process Builder was running actions when the Account record was changed so I had to put a flag on the Account record, which is updated to true the moment a batch job is created. This ensures only one batch is created for a given transaction. The code change is below for the AccountHelper class

/**
* Helper class for Account trigger handlers
*
* @author   Nelson Chisoko
* @created  2019/07/31
*/
public class AccountHelper {

    public static Id updateRetrievePhoenixPersonDetails(Map<Id, Account> accountsMap) {

        Set<Id> idsToProcess = new Set<Id>();
        Id batchProcessId;

        if (!System.isBatch()) {

            for (Account a : accountsMap.values()) {

                if (a.PersonEmail != null && a.Phoenix_People_Id__pc == null && !a.Async_Job_Created__c) {

                    idsToProcess.add(a.Id);

                }

            }

            if (!idsToProcess.isEmpty()) {

                batchProcessId = AsyncApexFramework.submitBatch(new GetPhoenixPersonDetailsBatch(idsToProcess), 100, null, false);

                AsyncApexFramework.flush();

                List<Account> accountsToUpdate = new List<Account>();

                for (Id accountId : idsToProcess) {

                    Account a = new Account();
                    a.Id = accountId;
                    a.Async_Job_Created__c = true;

                    accountsToUpdate.add(a);

                }

                try {

                    update accountsToUpdate;

                } catch (DmlException e) {

                    System.debug('An unexpected error has occurred: ' + e.getMessage());

                }

            }

        }

        return batchProcessId;

    }

}
1

Process Builder and Workflow Field Updates are two candidates for things that can cause your update trigger to run twice. Rollup summary fields can also cause you to run through triggers that you would otherwise not expect to be run.

Based on your AccountHelper, it does appear that you are appropriately detecting if you're in a batch context. That means that you have something that's causing your update trigger to be run twice.

The naive approach here might be to use trigger.old or trigger.oldmap to try to further limit the work done by AccountHelper so that you only perform your work when the value of your target fields are changing, or had an old value of X. This approach doesn't work when update triggers are run multiple times because trigger.old/trigger.oldmap is not updated after it is initially set at the beginning of the transaction (well, kinda... recursively called triggers make things a bit more murky)

The next approach I would take is to have a static Set<Id> alreadyProcessed declared in some apex class (it can't be in a trigger, since static variables declared inside of a trigger class itself are reset between contexts (before update -> after update) and between trigger chunks (one chunk of 200 records -> next chunk of 200 records).

The static set keeps track of what you've already seen, and allows you to process more than 200 records (the shortcoming of a simple static boolean is that if your trigger is working on more than 200 records, a static boolean would prevent the run of every record after #200 [or potentially fewer]).

Provided you have this static set, the changes required to your existing code would be minimal.

if (!System.isBatch()) {
    for (Account a : accountsMap.values()) {
        if (a.PersonEmail != null && a.Phoenix_People_Id__pc == null) {
            idsToProcess.add(a.Id);
        }
    }

    // If we've already processed a record, remove it from
    //   the list of things to process
    idsToProcess.removeAll(someClass.alreadyProcessed);

    if (!idsToProcess.isEmpty()) {
        // In here, we know that we haven't yet processed the records in idsToProcess
        // Add these ids to the static set so we know not to process them again
        someClass.alreadyProcessed.addAll(idsToProcess);

        batchProcessId = AsyncApexFramework.submitBatch(new GetPhoenixPersonDetailsBatch(idsToProcess), 100, null, false);
        AsyncApexFramework.flush();

    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.