1

Which one is the best way to optimize the performance if optimization available,

Option 1:

public without sharing class AccountTrigger extends TriggerHandler{
List<Id> accountIds;

public override void afterUpdate() {
    if (accountIds.size() > 0) {
        List<Case> casesToUpd = [select Id,AccountId from Case where AccountId in :accountIds];
        for (Case c: casesToUpd) {
            if((acctToAcctInfo.get(c.AccountId).split('%%%%%'))[0]==''||(acctToAcctInfo.get(c.AccountId).split('%%%%%'))[0]=='null'){
                c.Account_Important_Info__c = '' ;
            }else{
                c.Account_Important_Info__c = (acctToAcctInfo.get(c.AccountId).split('%%%%%'))[0] ;
            }

            if((acctToAcctInfo.get(c.AccountId).split('%%%%%'))[1]=='' || (acctToAcctInfo.get(c.AccountId).split('%%%%%'))[1]=='null'){
                c.Account_DTS_Info__c = '';
            }else{
                c.Account_DTS_Info__c = (acctToAcctInfo.get(c.AccountId).split('%%%%%'))[1] ;
            }
        }
        BatchApex btch = new BatchApex(casesToUpd);
        Database.executeBatch(btch,200);
        }
    }
}

global class BatchApex implements Database.Batchable<Case>{
    global final List<Case> cList;

   global BatchApex (List<Case> lst){
      //retrieve the case details from Account Trigger
      cList = new List<Case>(lst);
   }

   global Iterable<Case> start(Database.BatchableContext BC){
      return cList;
   }

   global void execute(Database.BatchableContext BC, List<Case> scope){
      update scope;
   }

   global void finish(Database.BatchableContext BC){
   }
}

Option 2:

public without sharing class AccountTrigger extends TriggerHandler{
List<Id> accountIds;

public override void afterUpdate() {
    if (accountIds.size() > 0) {
        BatchApex btch = new BatchApex(accountIds);
        Database.executeBatch(btch,200);
        }
    }
}

global class BatchApex implements Database.Batchable<Case>{
global final List<Id> acountIds;

global BatchApex (List<Id> lst){
  //retrieve the Account details from Account Trigger
  acountIds = new List<Id>(lst);
}

   global Database.QueryLocator start(Database.BatchableContext BC){
       String query = 'select Id,AccountId from Case where AccountId in :accountIds';
       return Database.getQueryLocator(query);
   }

   global void execute(Database.BatchableContext BC, List<Case> casesToUpd){
        for (Case c: casesToUpd) {
            if((acctToAcctInfo.get(c.AccountId).split('%%%%%'))[0]==''||(acctToAcctInfo.get(c.AccountId).split('%%%%%'))[0]=='null'){
                c.Account_Important_Info__c = '' ;
            }else{
                c.Account_Important_Info__c = (acctToAcctInfo.get(c.AccountId).split('%%%%%'))[0] ;
            }

            if((acctToAcctInfo.get(c.AccountId).split('%%%%%'))[1]=='' || (acctToAcctInfo.get(c.AccountId).split('%%%%%'))[1]=='null'){
                c.Account_DTS_Info__c = '';
            }else{
                c.Account_DTS_Info__c = (acctToAcctInfo.get(c.AccountId).split('%%%%%'))[1] ;
            }
        }
     update casesToUpd;
    }

   global void finish(Database.BatchableContext BC){
   }
}

I am expecting the optimize one but didn't know how to decide which one is the optimize one. getting the Case from the Trigger and pass to the Batch in the Option 1 however in the Option 2 I am getting the Case records in the start method(QueryLocator) of the Batch. Which one is the most optimize one?

1 Answer 1

6

I would recommend against both approaches because a trigger will be called multiple times (with 200 objects at a time) when you have large numbers of objects (in your case Accounts) being updated in one transaction. This will create multiple batches and you should be aware that you can queue at most only 100 batches (with 5 running too) in your org at any one time. You cannot assume this is the only batch being queued/executed so making this approach fragile.

Additionally, maintaining large batch state by holding whole objects (your list of Cases) can prove problematic since they could be updated before the batch processes them, so live querying them in the batch seems better to me.

The way we have addressed this sort of case is to:

  1. Introduce two Datetime timestamps on the object (for you, Account), the first "ProcessingLastRequired__c" and the second "ProcessingLastPerformed__c".
  2. In the (before update) trigger, each Account that needs processing in the batch has its ProcessingLastRequired__c set to System.now().
  3. If any Accounts have their timestamp set, we raise a Platform Event at the end of the trigger processing.
  4. The Platform Event is consumed by a trigger-based Platform Event consumer.
  5. This consumer checks to see if there is already a batch instance queued or scheduled. If there is, it exits silently.
  6. If no batch instance is queued or scheduled***, the consumer schedules the batch to execute in 1 minute.
  7. The batch's start checks there is no other instance of this batch already running. If there is, it returns an empty scope. Otherwise it returns an SOQL-based query locator that looks for Account instances where the ProcessingLastRequired__c is not null AND (ProcessingLastPerformed__c is null OR ProcessingLastRequired__c >= ProcessingLastPerformed__c)**
  8. The batch's execute processes the Account instances as needed and updates their ProcessingLastPerformed__c to be "now"
  9. The batch's finish queries to see if any Accounts would match the criteria used in start (doing much the same query but getting a COUNT) and if this count is non-zero, it attempts to re-schedule itself for 1 minute from now.

The 1 minute delay is to avoid consuming all available async executions per 24 hour period if something goes wrong in processing data.

We have loads of other things we do as well, such as maintaining lists of "bad items" so we can exclude them from queries, at least in the short term. We also made the framework for this more generic so we can have multiple "adaptive batches" for different purposes.

** The field comparison here is actually done by using a third field, a checkbox formula field, given SOQL query limitations.

*** You may want to ensure that any batch scheduled for execution where the execution time is significantly in the past is cancelled so a new one can be added. This is to work around a bug in the Salesforce scheduling mechanism.

4
  • Great answer. I would just add one additional check in step 6, when checking if a job is scheduled, make sure the next fire time is not in the past. We’ve had a problem where jobs scheduled to run in a short amount of time (1 to 5) minutes would get scheduled and never run. After some research it turned out to be a known Salesforce issue when load on that system is high. Oct 8, 2021 at 12:11
  • 1
    @HaroutTatarian take a look at this "idea" that Salesforce insisted I raised before they would consider fixing this issue. Indeed, we have seen 1 minute be too short on two customer orgs, in both cases during platform upgrade windows.
    – Phil W
    Oct 8, 2021 at 12:26
  • I've added a footnote for point 6 as you suggested.
    – Phil W
    Oct 8, 2021 at 12:30
  • 1
    What a coincidence ! Well I'm glad you raised the idea (the bug) cause that's the one I found during my research a few months back and it helped us figure out why things were not running. Oct 8, 2021 at 12:43

You must log in to answer this question.

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