When To Avoid Batch Apex And Use Queueable Apex In Salesforce

Salesforce has four asynchronous apex ( Queueable Apex, Scheduled ApexBatch ApexFuture Methods) to run code asynchronously to handle different need of business application. These apex is used for syncing data in two systems, handling longer jobs which will hit Salesforce Governor limits. This asynchronous apex can be used in different use case. This post is to explain where to avoid batch apex and use Queueable Class instead. Although there are lot of use cases where batch job is sufficient to handle processing.

This post will explain how to handle callout issue You have uncommitted work pending. Please commit or rollback before calling out

Use Case To Solve:

We have around 30K+ account records of health care providers, we need to validate address of all records using address API (any API like SmartyStreetGoogle PlacesZipCode ). After API call we need to update record with outcome of API call like address validation is successful or failed in ValidationStatus field. ValidationStatus is custom field in account object. All health care data are stored in account object.

Solution:

To solve this use case we can use batch apex which will handle 200 records (100 in case of callout in batch) in a single batch and batch job will be created for all records. As it will run in batch, we have to put some condition in batch SOQL so that it will not process same record again and again. So we can use query like

Select id, ValidationStatus__c from Account where ValidationStatus__c=false

This will only process record which is pending but there is another problem with this approach. As validation is not successful due to some reason that record will be pending and same records will be fetched in each job for process. This issue we can handle using adding another filter last modified date in SOQL

SOQL in Asynchronous Apex gives us the advantage of higher governor limits

Select id, ValidationStatus__c from Account where ValidationStatus__c=false and LastModifiedDate<>TODAY

Using this batch it will solve issue of not processing same record again and again. We will face another problem after that is , if we want to re-process pending record or error records after correction, it will not re-process those record on same day.

So what will be efficient solution for our use case. Can we use another asynchronous apex as we have lot of records to process? Yes, we have to use another asynchronous apex Queueable to handle this. We can query all pending records in one parent queueable class and then process records in another queueable class which will support callout. We will run chain of Queueable child class to process 100 records at once. Once 100 records are processed, start another queueable child job. This way we can call external system and make list of records to update before we start another job. This way we can avoid uncommitted work pending issue also.

Uncommitted work pending issue throws when we are calling external system and then updating response in object, when system start processing next record, previous records is still pending for commit so we get that error. This issue will be address by below approach

  1. We will call external callout for reach record one by one
  2. We will not update records immediately, instead we will add all records in collection
  3. Once 99 records are processed, we will start new queueable job for processing another set of 99 records
  4. Before we start new batch, we will update all records for status update.

Above approach will handle issue uncommitted work pending for commit issue.

Let us see code for this solution

public class AddressValidationQueueableRunner implements Queueable, Database.AllowsCallouts{
    public void execute(QueueableContext context) {
        integer counter=0;
        try
        {
            Map<Id,Account> recordMap=new Map<id,Account>([SELECT ID,ShippingStreet,ShippingCity,ShippingState,ShippingPostalCode,ValidationStatus__c from 
                                                           Account Where ShippingStreet!=null and ValidationStatus__c = false]);
            system.debug('record to process...'+recordMap.size());
            
            if(recordMap !=null && recordMap.size()>0){
				AddressValidationQueueable esync = new AddressValidationQueueable(recordMap);
                System.enqueueJob(esync);
            }
        }
        catch(Exception ex)
        {
            ApplicationException.logException(ex);
        }
    }
}

Above queueable apex code is getting all records which need to process and passing all records to another child queueable class. As we are passing all records to child queueable class we don’t require to run this class multiple time. So if any error occurs for any records and we want to re-process those records, we can run this class once again after record correction.

Let us see code for child queueable class which will process each record and also handle uncommitted code pending issue.

public class AddressValidationQueueable implements Queueable, Database.AllowsCallouts{
    public Integer MAX_RECORD = 99;
    public Integer processingCount = 0;
    Map<Id,Account> accounts;
    List<Account> accts;
    
    public AddressValidationQueueable(Map<Id,Account> acts)
    {
        accounts=acts;
    }
    public void execute(QueueableContext context) {
        integer counter=0;
        boolean startNewQueue=false;
        try
        {
            accts=new List<Account>();
            
            for(ID index:accounts.keySet())
            {
                Account act=accounts.get(index);
                if(!startNewQueue)
                {
                    if(act!=null)
                    {
                        try
                        {
                            accts.add(AddressVerification.processRecord(act));
                        }
                        catch(Exception ex)
                        {
                            system.debug('ex:'+ex.getMessage());
                            ApplicationException.LogException(ex);
                        }
                        counter=counter+1;
                        accounts.remove(index);
                    }
                    system.debug('accts:'+ counter);
                    if(counter>MAX_RECORD)
                    {
                        startNewQueue=true;
                        break;
                    }                         
                }
            }
            if(startNewQueue)
            { 
                processTargets(accts);
                AddressValidationQueueable esync = new AddressValidationQueueable(accounts);
                counter=0;
                System.enqueueJob(esync);
            }
        }
        catch(Exception ex)
        {
            ApplicationException.LogException(ex);
        }
    }
    
    public void processTargets(List<Account> accounts){
        if(accounts!=null && !accounts.isEmpty())
        {
            update accounts;
        }
    }
}

public class AddressVerification{
    
    public static string NAMED_CREDENTIAL ='SmartyStreet';
    
    public static Account processRecord(Account act)
    {   
        APIConfiguration__mdt mdt = APIConfiguration__mdt.getInstance('SmartyStreet');
        
        Map<String, String> ncParams;
        if(string.isNotBlank(act.ShippingCity) && string.isNotBlank(act.ShippingState))
        {
            ncParams=new Map<String, String> {
                	'auth-id' => mdt.UserName__c,
                    'auth-token' => mdt.Password__c, 
                    'street' => act.ShippingStreet,
                    'city' => act.ShippingCity,
                    'state' => act.ShippingState,
                    'zipCode' => act.ShippingPostalCode,
                    'candidates' => '10'
           	};
                        }
        else if(string.isNotBlank(act.ShippingPostalCode))
        {
            ncParams=new Map<String, String> {
                'auth-id' => mdt.UserName__c,
                    'auth-token' => mdt.Password__c, 
                    'street' => act.ShippingStreet,
                    'zipcode' => act.ShippingPostalCode,
                    'candidates' => '10'
                    };
                        }
        else
        {
            ncParams=new Map<String, String> {
                'auth-id' => mdt.UserName__c,
                    'auth-token' => mdt.Password__c, 
                    'street' => act.ShippingStreet,
                    'candidates' => '10'
                    }; 
                        }
        system.debug('Auth:' + ExternalCallout.AUTH_TYPE.NAMED_CREDENTIAL);
        Map<String, String> headers=new Map<String, String> {'Content-Type' => 'application/xml'};
        HttpResponse response=new ExternalCallout().get('SmartyStreet', ncParams,headers);
        system.debug('response:'+response.getBody());
        if(response!=null)
        {
            act.ValidationStatus__c=true;
        }
        return act;
    }
}

AddressVerification class do external callout to Smarty Street address API. External callout code is available in another blog Generic Apex class for Calling External System. To store API detail, I have used named credential SmartyStreet which will store API url https://us-street.api.smartystreets.com/street-address , username and password.

AddressValidationQueueable is child queueable class which will call AddressVerification class for each record and add those accounts in list. Once set of 99 records are processed, we will update those records and start new child queueable class to process next set of 99 records. We are also removing that record from list so that it will not process again.

Leave a Reply

Your email address will not be published. Required fields are marked *

×

 

Hello!

Click on my picture below to chat on WhatsApp

× Chat With Me