Apex Trigger Development

Best Practices

Presented by Scott Covert / @scottbcovert

Intro

Umm, my workflow rule can't do that-what should I do?

Common Issues

The world of Apex development ain't all sunshine and rainbows.

Context

Should my logic live in a before or after trigger?

Null reference? I thought all records have ids?

Bulk DML Operations

Umm, you mean you want to insert more than a single account at once?

SOQL inside For Loops

...what's a governor limit?

Logic inside Triggers

Unit tests?

Multiple Triggers per Object

Recursion

Spaghetti Code

...but it's sorta working

Best Practices

Context

Before:

  • Set/change field values before writing to database
  • No Id value yet for Insert triggers

After:

  • Id values can be safely mapped

Insert:

  • No Trigger.old or Trigger.oldMap

Update:

  • Can update original object (be careful of recursion)

Context Cheat Sheets

Trigger Context Variables

Context Variable Considerations

Bulkification

Don't Do This!


trigger accountTestTrggr on Account (before insert, before update) {

   //This only handles the first record in the Trigger.new collection
   //But if more than 1 Account initiated this trigger, those additional records
   //will not be processed
   Account acct = Trigger.new[0];
   List<Contact> contacts = [select id, salutation, firstname, lastname, email 
              from Contact where accountId = :acct.Id];
   
}
						

Bulkified Trigger


trigger accountTestTrggr on Account (before insert, before update) {

   List<String> accountNames = new List<String>{};
 
   //Loop through all records in the Trigger.new collection
   for(Account a: Trigger.new){
      //Concatenate the Name and billingState into the Description field
      a.Description = a.Name + ':' + a.BillingState
   }
   
}
					

Bulkification Cheat Sheet

Bulkify Your Code

No SOQL in For Loops

.

Don't Do This!


trigger accountTestTrggr on Account (before insert, before update) {
	  
      //For loop to iterate through all the incoming Account records
      for(Account a: Trigger.new){
      	  /*
      	    THIS FOLLOWING QUERY IS INEFFICIENT AND DOESN'T SCALE
      	    Since the SOQL Query for related Contacts is within the FOR loop, if this trigger is initiated 
      	    with more than 20 records, the trigger will exceed the trigger governor limit
            of maximum 20 SOQL Queries.
      	  */
      	  List<Contact> contacts = [select id, salutation, firstname, lastname, email 
                        from Contact where accountId = :a.Id];
 		  
 	  for(Contact c: contacts){
 	  	  System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], 
                                         LastName[' + c.lastname +']');
 	  	  c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname;
 	  	   /*
      	             THIS FOLLOWING DML STATEMENT IS INEFFICIENT AND DOESN'T SCALE
      	             Since the UPDATE dml operation is within the FOR loop, if this trigger is initiated 
      	             with more than 20 records, the trigger will exceed the trigger governor limit 
                     of maximum 20 DML Operations.
      	            */
      	      
 		  update c;
 	  }    	  
      }
}
						

Relationship Queries and Maps are Your Friends


trigger accountTestTrggr on Account (before insert, before update) {
  //This queries all Contacts related to the incoming Account records in a single SOQL query.
  //This is also an example of how to use child relationships in SOQL
  List<Account> accountsWithContacts = [select id, name, (select id, salutation, description, 
                                                                firstname, lastname, email from Contacts) 
                                                                from Account where Id IN :Trigger.newMap.keySet()];
	  
  List<Contact> contactsToUpdate = new List<Contact>{};
  // For loop to iterate through all the queried Account records 
  for(Account a: accountsWithContacts){
     // Use the child relationships dot syntax to access the related Contacts
     for(Contact c: a.Contacts){
   	  System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], LastName[' + c.lastname +']');
   	  c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname; 
   	  contactsToUpdate.add(c);
     }    	  
   }
      
   //Now outside the FOR Loop, perform a single Update DML statement. 
   update contactsToUpdate;
}
						

Removing SOQL from For Loops Cheat Sheet

Avoid SOQL Queries Inside FOR Loops

No Logic inside Triggers

Makes unit testing much easier

Keeps your codebase DRY

Don't Do This!


trigger updateContactOtherAddress on Account(after insert, after update) {  
    if (trigger.isUpdate) {
        //Identify Account Address Changes
        Set<Id> setAccountAddressChangedIds = new Set<Id>();

        for (Account oAccount : trigger.new) {
            Account oOldAccount = trigger.oldMap.get(oAccount.Id);

            boolean bIsChanged = (oAccount.BillingStreet != oOldAccount.BillingStreet || oAccount.BillingCity != oOldAccount.BillingCity);
            if (bIsChanged) {
                setAccountAddressChangedIds.add(oAccount.Id);
            }
        }

        //If any, get contacts associated with each account
        if (setAccountAddressChangedIds.isEmpty() == false) {
            List<Contact> listContacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :setAccountAddressChangedIds];
            for (Contact oContact : listContacts) {
                //Get Account
                oAccount = trigger.newMap.get(oContact.AccountId);

                //Set Address
                oContact.OtherStreet = oAccount.BillingStreet;
                oContact.OtherCity = oAccount.BillingCity;
                oContact.OtherState = oAccount.BillingState;
                oContact.OtherPostalCode = oAccount.BillingPostalCode;
            }

            //If any, execute DML command to save contact addresses
            if (listContacts.isEmpty() == false) {
                update listContacts;
            }
        }
    }
}
						

Much Cleaner


trigger AccountBeforeEventListener on Account (before insert, before update) {  
    if (trigger.isInsert) {
        AccountUtil.setDefaultValues(trigger.new);
        AccountUtil.setIndustryCode(trigger.new);
    }
}
						

public class AccountUtil {  
    public static void setDefaultValues(List<Account> listAccounts) {
        for (Account oAccount : listAccounts) {
            if (oAccount.Industry == null) {
                oAccount.Industry = ‘Cloud Computing’;
            }
        }
    }

    public static void setIndustryCode(List<Account> listAccounts) {
        for (Account oAccount : listAccounts) {
            if (oAccount.Industry == ‘Cloud Computing’) {
                oAccount.Industry_Code__c = ‘CC’;
            }
        }
    }
}
						

Removing Logic from Triggers Cheat Sheet

Apex Trigger Best Practices: Ignorant Triggers

Centralized Framework

No more:

  • Order of Execution Woes
  • Recursion Scares
  • Spaghetti Code

Options

Centralized Framework Cheat Sheet

Trigger Frameworks

Clicks Not Code

Next Steps