Home SalesforceApex Apex Trigger Code Optimization

Apex Trigger Code Optimization

by Dhanik Lal Sahni

Apex triggers enable us to perform some custom actions before or after changes to Salesforce records. These changes can be record insertions, updates, or deletions. As these are firing before or after any record changes, it will affect performance, if the trigger code is not optimized. This post will provide different ways to Apex Trigger Code Optimization.

Let us see problems and solutions which we can implement in Apex Trigger to solve the performance issue.

1. Avoid SOQL In Loop

This is a common issue that I have found while reviewing the code of many developers. Developer fetches query in the loop considering their current use case. They think that we need to perform some action for record change which will be performed on UI.

trigger InsuranceFormTrigger on InsuranceForm__c (before insert, before update) {
	for(InsuranceForm__c m : Trigger.new){ 
	   	Account a = [SELECT Id FROM Account WHERE Name= m.AccountId__c];
		//Some code Action here
	}
}

This is a very simple use case, where the developer is trying to get Account information for the current InsuranceForm record. If we see this code, it looks good but what if we have to upload multiple records using a data loader or any other tool. Will that code work..? Absolutely not, as the query will be fetched in the loop and it will throw an error just after 200 records.

Optimized Code :

We have to take SOQL out from the loop and collect all required IDs in the loop first and then we should use SOQL.

trigger InsuranceFormTrigger on InsuranceForm__c (before insert, before update) {
	
	Set accountIds=new Set();

	//Collect AccountIds first
	for(InsuranceForm__c m : Trigger.new){ 
		//Avoid nulls
		if(string.isNotBlank(m.AccountId__c))
		{
			accountIds.add(m.AccountId__c); 
		}
	}

	//Ids exist then proceed further
	if(!accountIds.isEmpty())
	{
		Account a = [SELECT Id FROM Account WHERE Id=:accountIds];
		//Some Action here
	}
}

We should always avoid nulls in the query filter so add code to put Ids in Set only when it has value.

2. Avoid Loop to get changed Records

Many developers use for loop to just identify record Ids that are changed or newly inserted like the below code.

trigger InsuranceFormTrigger on InsuranceForm__c (after insert, after update) {
{
	Set newRecords=new Set();
	for(InsuranceForm__c m : Trigger.new){ 
		newRecords.add(m.Id); 
	}
}

It is not required as there is already a global trigger variable (Trigger.newMap) to provide a newly inserted/updated sObject. The above trigger code can easily be changed like below.

trigger InsuranceFormTrigger on InsuranceForm__c (after insert, after update) {
{
	Set ids = Trigger.newMap.keySet();
	//Trigger.newMap is Map of Id and sObject. Use ids in some action or other query
}

3. Avoid Hardcoded Id

While writing code sometimes we use hardcoded id like record type id, profile id, or permission set id to test functionality. We forget to change it and this hardcode id gives undesirable output while running our application in another org.

It will throw an error also when we were moving our code to a different org and the test class will validate. Those hardcode IDs will not be available in the test context of another org, so we will get errors.

for(Case c: Trigger.new){
   if(c.RecordTypeId=='0124000000095uy'){ // Lead Case
      // ...
   } else if(c.RecordTypeId=='0126000000095Gmwer'){ // Support Case
      // ...
   }
}

In the above code sample, RecordTypeId – 0124000000095uy and 0126000000095Gm are hardcoded and this code will only work in the current org. This code will throw an error when we create a test class or when we move this to another org.

Optimized Code:

Instead of using a hardcoded id, we can utilize a record type name to retrieve the record type id in the current org. So above code can be written like below

//Case Record Type Id based on Record Type Name
Id leadCaseTypeId= Schema.SObjectType.Case.getRecordTypeInfosByName().get('Lead Case').getRecordTypeId();
Id supportCaseTypeId= Schema.SObjectType.Case.getRecordTypeInfosByName().get('Support Case').getRecordTypeId();

for(Case c: Trigger.new){
   if(c.RecordTypeId==leadCaseTypeId){ // Lead Case
      // ...
   } else if(c.RecordTypeId==supportCaseTypeId){ // Support Case
      // ...
   }
}

If your application code is using other then RecordTypeID hardcoded id, then you can utilize Custom Setting, Custom Metadata Type, or Custom Label as well to make that code dynamic.

4. Avoid Recursive Call

Many times we are creating triggers to update the same record after some processing. This can create a recursive trigger call and the system will throw Maximum trigger depth exceeded , if we have not implemented the code properly.

Let us take an example, When a new file is uploaded to Account record, we will create a Case to approve this file. This record will go under the approval process. When the case is approved, we will close the case and update a few other details in the case and account record.

In the above case as soon as the Case is approved, we have to close this case and update the fields. We have to use trigger here to run code as soon as the case is approved.

trigger CaseTrigger on Case (after update) {
{
	List cases=new List();
	for(Case c: Trigger.new){
		if(c.Stage__c=='Approved')
		{
			c.Status='Closed';
		}
		cases.add(c);
		//Adding case in list in all situation 
	}
	update cases;
}

In the above code, we are checking Case.Stage__c field and updating Status field as well but we are adding current iterative case in the list (cases.add(c)) every time. So when the update will fire, the same record will update and it will execute this trigger code block once again and this will keep calling the same code.

Optimized Code:

trigger CaseTrigger on Case (after update) {
{
	List cases=new List();
	for(Case c: Trigger.new){
		//Status will only updated when it is not Closed
		//So after Case is updated and same code is execute then this block condition will not met
		//and not executed again
		if(c.Stage__c=='Approved' && c.Status!='Closed')
		{
			c.Status='Closed';
			cases.add(c);
			//Adding case in list
		}
	}
	if(!cases.isEmpty())
	{
		update cases;
	}
}

Similar to this there can be any situation where you are updating the same record and it is firing the update record statement again and again for the same record. Put some conditions so that it will not do a recursive call. In the above example, I have used c.Status!=’Closed’ to restrict record update. There is another solution outlined at Apex Trigger Best practices to avoid recursion for recursive trigger issues.

5. Use One trigger per object

Many times developer asks, I have created 2 or n triggers on the same object, which trigger will fire first? And we can not give an exact answer to this question as we can not determine the trigger execution order. This question is asked by developers to verify their code will be executed in which order. The solution for this question is to create one trigger per object and put your code in proper order. So that your code will be executed as per your requirement.

How to make one trigger per object?

In trigger, we can write code related to record insert, update, delete, undelete, upsert or merge-operations. We can perform an action before and after these operations.

So we have to create a separate trigger handler class and add separate methods for each action and record operation. So take an example for account object we have to write trigger like this.

trigger AccountTrigger on Account (
    before insert, after insert, 
    before update, after update, 
    before delete, after delete,
    after undelete
) {
    
    if (Trigger.isBefore) {
        if (Trigger.isInsert) {
            // Call Trigger handler class method for before insert operation
        } 
        if (Trigger.isUpdate) {
            // Call Trigger handler class method for before update operation
        }
        if (Trigger.isDelete) {
            // Call Trigger handler class method for before delete operation
        }
    }
    
    if (Trigger.isAfter) {
        if (Trigger.isInsert) {
            // Call Trigger handler class method for after insert operation
        } 
        if (Trigger.isUpdate) {
            // Call Trigger handler class method for after update operation
        }
        if (Trigger.isDelete) {
             // Call Trigger handler class method for after delete operation
        }
        
        // Not supported for all objects
        if (Trigger.isundelete) {
             // Call Trigger handler class method for after undelete operation
        }
    }
}

As per the above code, for each action, we have to create a separate trigger handler method. If you don’t have a use case for any action then delete that code block. For example, if no custom action is required on delete then remove Trigger.isDelete code block from code. Let us create a trigger handler class and update the trigger.

flagPractionerVerification is called before insert opertaion and closeAllTasks method is called after insert operation. If we want to add other methods or code blocks after or before these methods, we can easily add those methods. This way we can create order execution.

Similar to flagPractionerVerification and closeAllTasks other methods can be created in the handler class and called from a specific record trigger operation.

Benefits of this pattern:

  1. Can control order execution of code
  2. Cleaner Code
  3. Easy to create a test class
  4. Maintainable code

References:

Trigger Syntax

Are there ever exceptions to the one trigger per object rule?

Related Articles:

Optimize SOQL Filter in Apex Code

Optimize Apex Code by Metadata Caching

Optimize Code by Disabling Debug Mode

Optimizing Loop in Apex Code

You may also like

Leave a Comment