Tuesday, April 30, 2019

How to call future(asynchronous class in apex) methods from salesforce apex triggers and not run into governor limits


 Use @future Appropriately
     As articulated throughout this article, it is critical to write your Apex code to efficiently handle bulk or many records at a time. This is also true for asynchronous Apex methods (those annotated with the @future keyword).
          Even though Apex written within an asynchronous method gets its own independent set of higher governor limits, it still has governor limits. Additionally, no more than ten @future methods can be invoked within a single Apex transaction.

·    Here is a list of governor limits specific to the @future annotation:
·    No more than 10 method calls per Apex invocation
·    No more than 200 method calls per Salesforce license per 24 hours.
·    The parameters specified must be primitive dataypes, arrays of primitive  datatypes, or collections of primitive datatypes.
·    Methods with the future annotation cannot take sObjects or objects as arguments.
·    Methods with the future annotation cannot be used in Visualforce controllers in either getMethodName or setMethodName methods, nor in the constructor.
 the Apex trigger inefficiently invokes an asynchronous method for each Account record it wants to process:
EX :
trigger accountAsyncTrigger on Account (after insert, after update) {
  for(Account a: Trigger.new){
    // Invoke the @future method for each Account
    // This is inefficient and will easily exceed the governor limit of
    // at most 10 @future invocation per Apex transaction
    asyncApex.processAccount(a.id);
   }    
}
Here is the Apex class that defines the @future method:
EX :
global class asyncApex {

  @future
  public static void processAccount(Id accountId) {
       List<Contact> contacts = [select id, salutation, firstname, lastname, email
                from Contact where accountId =&nbsp;:accountId];
            
         for(Contact c: contacts){
              System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], LastName[' + c.lastname +']');
                                                    c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname;
        }
        update contacts;       
  }  
}

Since the @future method is invoked within the for loop, it will be called N-times (depending on the number of accounts being processed). So if there are more than ten accounts, this code will throw an exception for exceeding a governor limit of only ten @future invocations per Apex transaction.

Instead, the @future method should be invoked with a batch of records so that it is only invoked once for all records it needs to process:
EX:
trigger accountAsyncTrigger on Account (after insert, after update) {
    //By passing the @future method a set of Ids, it only needs to be
    //invoked once to handle all of the data.
    asyncApex.processAccount(Trigger.newMap.keySet());
}

And now the @future method is designed to receive a set of records:
EX :
global class asyncApex {
   @future
  public static void processAccount(Set<Id> accountIds) {
       List<Contact> contacts = [select id, salutation, firstname, lastname, email from Contact where accountId IN&nbsp;:accountIds];
       for(Contact c: contacts){
           System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], LastName[' + c.lastname +']');
                                  c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname;
        }
        update contacts;
  }
}
Writing Test Methods to Verify Large Datasets :
             Here is the poorly written contact trigger. For each contact, the trigger performs a SOQL query to retrieve the related account. The invalid part of this trigger is that the SOQL query is within the for loop and therefore will throw a governor limit exception if more than 100 contacts are inserted/updated.
EX :
trigger contactTest on Contact (before insert, before update) {
             
             for(Contact ct: Trigger.new){
              Account acct = [select id, name from Account where Id=:ct.AccountId];
                 if(acct.BillingState=='CA'){
                    System.debug('found a contact related to an account in california...');
                    ct.email = 'test_email@testing.com';
                    //Apply more logic here....
                 }
             }
}
Here is the test method that tests if this trigger properly handles volume datasets:
EX :
public class sampleTestMethodCls {
              static testMethod void testAccountTrigger(){
                  //First, prepare 200 contacts for the test data
                  Account acct = new Account(name='test account');
                  insert acct;          
                  Contact[] contactsToCreate = new Contact[]{};
                  for(Integer x=0; x<200;x++){
                      Contact ct = new Contact(AccountId=acct.Id,lastname='test');
                      contactsToCreate.add(ct);
                  }
                  //Now insert data causing an contact trigger to fire.
                  Test.startTest();
                  insert contactsToCreate;
                  Test.stopTest();  
              } 
          }
Note the use of Test.startTest and Test.stopTest. When executing tests, code called before Test.startTest and after Test.stopTest receive a separate set of governor limits than the code called between Test.startTest and Test.stopTest. This allows for any data that needs to be setup to do so without affecting the governor limits available to the actual code being tested.

Now let's correct the trigger to properly handle bulk operations. The key to fixing this trigger is to get the SOQL query outside the for loop and only do one SOQL Query:
EX :
trigger contactTest on Contact (before insert, before update) {              
             Set<Id> accountIds = new Set<Id>();
             for(Contact ct: Trigger.new)
                 accountIds.add(ct.AccountId);             
             //Do SOQL Query   
             Map<Id, Account> accounts = new Map<Id, Account>(
                  [select id, name, billingState from Account where id in :accountIds]);                
             for(Contact ct: Trigger.new){
                 if(accounts.get(ct.AccountId).BillingState=='CA'){
                     System.debug('found a contact related to an account in california...');
                     ct.email = 'test_email@testing.com';
                     //Apply more logic here....
                 }
             }
          }
Note how the SOQL query retrieving the accounts is now done once only. If you re-run the test method shown above, it will now execute successfully with no errors and 100% code coverage.
Avoiding Hardcoding :
                    Here is a sample that hardcodes the record type IDs that are used in an conditional statement. This will work fine in the specific environment in which the code was developed, but if this code were to be installed in a separate org (ie. as part of an AppExchange package), there is no guarantee that the record type identifiers will be the same.
EX :
for(Account a: Trigger.new){
             //Error - hardcoded the record type id
             if(a.RecordTypeId=='012500000009WAr'){         
                //do some logic here.....
             }else if(a.RecordTypeId=='0123000000095Km'){
                //do some logic here for a different record type...
             }
       }
}       
           Now, to properly handle the dynamic nature of the record type IDs, the following example queries for the record types in the code, stores the dataset in a map collection for easy retrieval, and ultimately avoids any hardcoding.
EX :
//Query for the Account record types
               List<RecordType> rtypes = [Select Name, Id From RecordType
                            where sObjectType='Account' and isActive=true];
               //Create a map between the Record Type Name and Id for easy retrieval
               Map<String,String> accountRecordTypes = new Map<String,String>{};
               for(RecordType rt: rtypes)
                  accountRecordTypes.put(rt.Name,rt.Id);
                for(Account a: Trigger.new){
                    //Use the Map collection to dynamically retrieve the Record Type Id
                    //Avoid hardcoding Ids in the Apex code
                    if(a.RecordTypeId==accountRecordTypes.get('Healthcare')){           
                       //do some logic here.....
                    }else if(a.RecordTypeId==accountRecordTypes.get('High Tech')){
                       //do some logic here for a different record type...
                    }
               }  


No comments:

Post a Comment