Given a scenario, follow best practices to write Apex classes and triggers.
After studying this topic, you should be able to:
- Determine the different types of Apex triggers and how to write them using recommended design patterns
- Identify the best practices for writing Apex class and triggers and use them in a given scenario
Table of Contents
- Apex Classes
- Access and Definition Modifiers
- Sharing modes
- Class Variables and Methods
- Apex Triggers
- Trigger Context Variables
- Trigger Event Examples
- Trigger Design Patterns
- Serialize/Deserialize an Apex Class
- Platform Events
- Apex Class & Trigger Best Practices
- Scenarios and Solutions
Introduction
- Apex classes consist of variables and methods designed to support its intended functionality or purpose
- Example purposes/functionality: model for an object, controller for a user interface component, etc
- Apex triggers enable performing custom actions before or after a DML event like an insert, update, or delete operation
- Apex is like Java: it is strongly-typed, implements object-oriented principles and resembles Java’s syntax
Apex Classes
- Apex Classes are a template or blueprint from which objects in Apex are created
- An “object” is an instance of a class
- Technical use cases below require creation of an Apex class:
- Models and Actions: create a data model or perform custom actions in general
- Controller Class: server-side controller for a Visualforce page or custom Lightning component
- Testing Custom Code: design, build, perform unit tests
- Test Data Factory: create reusable component for test data generation
- Trigger handler: separate out business logic invoked by an Apex trigger
- Implmenting Interfaces: implement inheritance using an interface
- Extending classes: implement inheritance by extending a virtual class
- Apex Class Definition - following are required:
- Class Name: comes after the
class
keyword - Access Modifier: required except for inner classes (classes defined inside other classes)
- Extend/Implement: class is allowed to extend another class or implement one or more interfaces
- Definition Modifier: optional
virtual
andabstract
definition modifiers - Sharing Modes: optional
inherited sharing
,with sharing
, orwithout sharing
sharing modes
- Class Name: comes after the
- Constructors - default, no-argument, public constructor is automatically available
- Multiple constructors with different parameters can be created
- Constructors do not have an explicit return type
// class with multiple constructors
public class ClassName {
// constructor with no arguments
public ClassName() { /* code block 1 */ }
// constructor with a single argument
public ClassName(Integer i) { /* code block 2 */ }
// constructor with multiple arguments
public ClassName(Integer j, String str) { /* code block 3 */ }}
// Object instantiation using different constructors
ClassName object1 = newClassName();
ClassName object2 = newClassName(1);
ClassName object3 = newClassName(2, 'Martin');
- An inner class can be defined inside a top-level class, then called the outer class
- If unspecified, inner classes default to private and is only accessible to the outer class
- Inner classes can only be one-level deep - cannot nest another class in an inner class
public class AnOuterClass {
// outer class code here
class AnInnerClass {
// inner class code here
/* class AnotherInnerClass { // this code will cause an error } */
}
}
- Classes can be written in Salesforce using the Apex Classes page in Setup, using Visual Studio Code in a Salesforce DX project, or in the Developer Console
- Popular use cases of Apex Classes:
- Web services
- Email services
- Complex validation over multiple objects
- Complex business processes that are not supported by workflow
- Custom transactional logic
- Batch operations
Access and Definition Modifiers
- Access modifiers determine how accessible a method or variable is to code outside the container class:
- Private: default level access, meaning the method or variable is accessible only within the Apex class in which it is defined
- Public: means the method or variable can be used by any Apex code in this application or namespace
- Protected: means the method or variable is visible to any inner classes in the defining Apex class, and to the classes that extend the defining Apex class
- Global: means the method or variable can be used by any Apex code that has access to the class, not just the Apex class in the same application or namespace
- Access modifiers are mandatory for outer classes, optional for inner classes
- Definition modifiers are optional and provide behavioral functionality when used on a class
- Virtual: declares the class allows extensions and overrides
- Abstract: declares the class contains abstract methods which only have signature and no body definition
- Access and definition modifier considerations:
- Private variables can be set from outside the Apex class through a public method
- Definition modifiers (Virtual and Abstract) are optional
global class Person {
private String name;
public void setName(String name) {
this.name = name;
}
}
Sharing Modes
- Apex code runs in system context by default, which allows it access to all data (all objects and fields) in the org
- System context means object permissions, field-level security and sharing rules of the current user are ignored
- Following sharing modes are used to explicitly define whether an Apex class respects data access restrictions
- with sharing: Apex class will enforce the sharing rules of the current user
- without sharing: Apex class does not respect sharing rules and runs in system context
- inherited sharing: Apex class inherits the sharing settings of the calling class or depends on how Apex class is used
- inherited sharing: the sharing mode is determined by the calling class or how the Apex class is used:
- If a class that respects sharing rules calls a class that uses inherited sharing, the called class will also respect sharing
- A class that uses inherited sharing will run as with sharing when used as:
- Visualforce or Lightning component controller
- An Apex Rest service
- Entry point to an Apex transaction
- Some considerations:
- Anonymous blocks: always executed using permissions of current user regardless of the sharing mode defined in a class
- Lightning components: Apex classes used by custom Lightning components respect sharing rules by default
// ignores the sharing model of the running user and
// will be able to access all data
public without sharing class IgnoreSharingSettings {}
// respects the sharing model and can only access
// the data that the current user is allowed to access
public with sharing class RespectSharingSettings {}
// uses the sharing model of the caller class
public inherited sharing class CallerSharingSettings {}
- Sharing settings can be defined on the outer class and also in the inner class
- Note inner classes do not inherit the sharing settings of their outer class
public with sharing class myOuterClass {
// myOuterClass code here
without sharing class myInnerClass {
// my InnerClass code here
}
}
Class Variables and Methods
- Class variables are used for storing values
- Declaration Syntax: Declare by specifying the data type and variable name
- Possible data types:
- Primitives: String, Integer, Boolean, sObjects, etc
- Collections: List, Set, Map
- User-Defined data types, using Apex classes
- Access modifiers such as public/private/protected/global define the data accessibility - defaults to private
- Optional keywords: final, static
- Null as Default: if no value is specified when declaring the value defaults to null
- Return value: data type for returned value must be explicitly specified for methods that return. Otherwise, specify void in the method definition.
- Arguments: method parameters should be separated by commas where each parameter is preceded by its data type. Empty parens
()
if the method takes no parameters. - Method Body: body of method containing code, including local variable declarations, enclosed in
{}
static
methods and variables provide certain behavioral functionality:- Shared variable: static variables store information that is shared across instances of the class
- No Instance Required: a static method or variable does not require a class to be instantiated in order to be accessed
- Utility Method: static methods can be called without instantiating the class
- Initialization: static member variables are initialized before an object of a class is created
- Outer Classes: static variables and methods can only be used with outer classes
- Example below shows how a static method and variable can be accessed without instantiating the class due to the static keyword
public class WeatherClass {
public static String weather = 'cloudy';
public static void isSunny() {
weather = 'sunny';
System.debug('The weather today is ' + weather);
}
}
System.debug(WeatherClass.weather); // prints 'cloudy'
WeatherClass.isSunny(); // prints 'The weather today is sunny'
Apex Triggers
- Triggers are used to execute Apex code when a DML event has occurred on an sObject
- Before Triggers
- Ex:
before update
,before update
,before delete
- Use when:
- Complex validation needs to be performed before allowing a record to be saved or deleted
- Field values of aa record need to be set or modified before it is saved
- Ex:
- After Triggers
- Ex:
after insert
,after update
,after delete
,after undelete
- Use when:
- Additional complex operations need to be performed after a record is saved, such as create, update or delete records in another object
- Field values of newly saved records need to be accessed, such as record
Id
- Ex:
- Before Triggers
- General info and considerations for writing Apex triggers:
- Trigger tools: Triggers can be created from the Salesforce UI, developer console, or Visual Studio Code
- Single or multiple: can be configured to handle single or multiple database events
- Handle multiple events: best practice is to define one trigger per object and handle multiple events within the trigger
- Context Variables: use trigger context variables to access record values and info about the trigger context
- Process in bulk: ensure the trigger handles multiple records efficiently by bulkifying the process
- Avoid governor limits: ensure the trigger execution context is understood so that governor limits are not exceeded
Trigger Context Variables
- Triggers have run-time context info for which can be accessed through context variables available in
System.Trigger
class. Examples:- Get Record Values: trigger context variables allow access to values in records prior or after they are saved to the database
- Get Running Context: trigger context variables allow access to information about the context in which the trigger is running
- Trigger DML Event:
Trigger.isInsert
,Trigger.isUpdate
,Trigger.isDelete
andTrigger.isUndelete
can be used to determine the DML event type that fired the trigger - Trigger Timing:
Trigger.isBefore
andTrigger.isAfter
can be sued to determine if the trigger was fired before or after all records were saved Trigger.isExecuting
: can be used to determine if the current context of an Apex code that is being executed is a triggerTrigger.old
: returns a list of the old versions of the sObject records- Only available in update and delete triggers
Trigger.new
: contains a list of the new versions of sObject records that is available in insert, update, and undelete triggersTrigger.operationType
: gets context about the current DML operation. Returns an enum of typeSystem.TriggerOperation
that corresponds to the current operationTrigger.oldMap
: contains a map of the previous versions of the sObject recordsTrigger.newMap
: contains a map of the updated versions of the sObject recordsTrigger.size
: determines the number of records processed in a trigger- Note triggers execute on batches of 200 records at a time
Trigger.new Example
trigger ContactTrigger on Contact (before insert) {
for (Contact con: Trigger.new) {
System.debug('Inserting new contact: ' + con.Name);
//...
}
}
Trigger.operationType Example
// using isBefore and isUpdate
if (trigger.isBefore && trigger.isUpdate) {
// Before Update logic here
}
// using operationType
switch on trigger.operationType {
when BEFORE_UPDATE {
// Before Update logic here
}
}
Trigger.oldMap Example
trigger AccountTrigger on Account (before update) {
for (Account newAccount: Trigger.new){
// The below will return the old version of the record.
// This is usually done to compare new and old field values
Account oldAccount = Trigger.oldMap.get(newAccount.Id);
}
}
Trigger.isExecuting Example
trigger ContactTrigger on Contact (before insert, before update) {
ContactHandler handler = newContactHandler();
if (Trigger.isBefore) {
if (Trigger.isInsert) {
handler.handleBeforeInsert(Trigger.new);
} elseif (Trigger.isUpdate) {
// perform before update logic here...
}
}
}
public class ContactHandler {
public void handleBeforeInsert(List<Contact> contacts) {
if (Trigger.isExecuting) {
// The method is called from an Apex trigger invocation.
// So, perform specific logic for this context...
}
if (System.isBatch()) {
// The method is called in a Batch Apex transaction.
// So, perform specific logic for this context...
}
}
// ...
}
Trigger Event Examples
- Before Insert trigger example: automatically sets the record type of a new record based on the selected value of a picklist field
trigger InquiryTrigger on Inquiry__c (before insert) {
Map<String, RecordType> invRecTypeMap = newMap<String, RecordType>();
for (RecordType recType: [SELECT Id, DeveloperName
FROM RecordType
WHERE SObjectType = 'Inquiry__c']) {
invRecTypeMap.put(recType.Name, recType);
}
for (Inquiry__c inquiry: Trigger.new) {
if (invRecTypeMap.containsKey(inquiry.Industry__c)) {
inquiry.RecordTypeId = invRecTypeMap.get(inquiry.Industry__c).Id;
}
}
}
- Before Update trigger example: used to assign the record to another owner whenever the value of its “Status” field is changed
trigger RequestTrigger on Request__c (before update) {
for (Request__c newRequest: Trigger.new) {
Request__c oldRequest = Trigger.oldMap.get(newRequest.Id);
if (newRequest.Status__c != oldRequest.Status__c) {
// change Owner based on the Status__c field
// newRequest.Owner = userId;
}
}
}
- After Update trigger example: attempting to make changes to Trigger.new records will result in a Final Exception at run-time
trigger OpportunityTrigger on Opportunity (after update) {
for (Opportunity opp: Trigger.new) {
opp.Description = 'automated description';
}
update opp;
}
- Before Delete trigger example: used to check and prevent the deletion of a contact if it is not associated with an account
trigger ContactTrigger on Contact (before delete) {
for (Contact con: Trigger.old) {
if (con.AccountId == null) {
con.addError('Unable to delete a Contact with no Account!');
}
}
}
- After Delete trigger example: used to update a field on the related account when a contact is deleted
- When a primary contact is deleted, the trigger sets a Has Primary Contact checkbox field on the related account to false
trigger ContactTrigger on Contact (before update, after update, before delete, after delete) {
List<Id> accountIds = newList<Id>();
if (Trigger.isAfter && Trigger.isDelete) {
// get all acount Ids to be updated
for (Contact c : Trigger.Old) {
if (c.Is_Primary_Contact__c == true) {
accountIds.add(c.AccountId);
}
}
// query and updated all accounts
if (accountIds.isEmpty() == false) {
List<Account> accounts = newList<Account>();
for (Account a : [SELECT Id, Has_Primary_Contact__c
FROM Account
WHERE Id IN :accountIds]) {
a.Has_Primary_Contact__c = false;
accounts.add(a);
}
update accounts;
}
}
}
Trigger Design Patterns
- Following design patterns result in more scalable, maintainable, and performant triggers:
- One trigger per object:
- One trigger is created on an object for all possible events
- A class can store the logic for the trigger, making it logic-less
- Allows for controlling the order of execution
- Helps avoid exceeding limits due to multiple triggers
- Trigger handler class:
- Trigger delegates logic to a handler class
- Handler methods are created based on context
- Routing logic using if-else statements can be used
- New functionality can be added without modifying the trigger
- Bulk triggers:
- Sets and maps are used for bulk processing of records
- Trigger is designed to process thousands of records at once
- One DML statement is used for operations
- Trigger minimizes DML calls to not exceed governor limits
- One trigger per object:
One trigger per object
- A single trigger is all that is needed essentially for any object
- Handler class: Create a handler class to store the logic for each trigger
- Reusable logic: class logic can be reused elsewhere like Visualforce pages/test classes
- Order of Execution: if multiple triggers are developed for a single object, there is no way of controlling the order of execution
- Governor Limits: multiple triggers per object can result in exceeding governor limits
- One trigger for all trigger events - trigger below handles multiple DML events and uses a trigger handler class to process the records
trigger AccountTrigger on Account (before insert, after insert,
before update, after update,
before delete, after delete,
after undelete) {
if (trigger.isBefore) {
if (trigger.isInsert) AccountTriggerHandler.onBeforeInsert(trigger.new);
if (trigger.isUpdate) AccountTriggerHandler.onBeforeUpdate(trigger.old,
trigger.oldMap,
trigger.new,
trigger.newMap);
if (trigger.isDelete) AccountTriggerHandler.onBeforeDelete(trigger.old,
trigger.oldMap);
}
if (trigger.isAfter) {
if (trigger.isInsert) AccountTriggereandler.onAfterInsert(trigger.new,
trigger.newMap);
if (trigger.isUpdate) AccountTriggerHandler.onAfterUpdate(trigger.old,
trigger.oldMap,
trigger.new,
trigger.newMap);
if (trigger.isDelete) AccountTriggerHandler.onAfterDelete(trigger.old,
trigger.oldMap);
if (trigger.isUndelete) AccountTriggerHandler.onAfterDelete(trigger.new,
trigger.newMap);
}
}
Trigger Handler Class
- Handler class performs all the logic processing for the trigger
- Delegate logic: trigger should be logic-less and delegate the logic responsibilities
- Handler methods: methods in a handler class can be created based on the context of the intended logic
- Routing logic: if-else statements can be used for calling each handler method
- Modifications: when new functionality is needed, the related code can be added to the handler class without modifying the trigger
trigger Opportunitytrigger on Opportunity (before insert, after insert,
before update, after update,
before delete, after delete) {
OpportunityTriggerHandler oppHandler = newOpportunityTriggerHandler();
if (trigger.isBefore){
if (trigger.isInsert){
oppHandler.beforeInsert(trigger.New);
} else if (trigger.isUpdate){
oppHandler.beforeUpdate(trigger.New, trigger.Old);
} else if (trigger.isDelete){
oppHandler.beforeDelete(trigger.Old);
}
} elseif (trigger.isAfter){
if(trigger.isInsert){
oppHandler.afterInsert(trigger.New);
} elseif (trigger.isUpdate){
oppHandler.afterUpdate(trigger.New, trigger.Old);
} elseif (trigger.isDelete){
oppHandler.afterDelete(trigger.Old);
}
}
}
public class OpportunityTriggerHandler {
public void beforeInsert(List<SObject> so) { // before insert logic here
List<Opportunity> opp = (List<Opportunity>) so;
}
public void beforeUpdate(List<SObject> so, List<SObject> oldSo){ // before update logic here
List<Opportunity> opp = (List<Opportunity>) so;
List<opportunity> oppOld = (List<Opportunity>) oldSo;
}
public void beforeDelete(List<SObject> oldSo){ // before delete logic here
List<Opportunity> oppOld = (List<Opportunity>) oldSo;
}
public void afterInsert(List<SObject> so){ // after insert logic here
List<Opportunity> opp = (List<Opportunity>) so;
}
public void afterUpdate(List<SObject> so, List<SObject> oldSo){ // after update logic here
List<Opportunity> opp = (List<Opportunity>) so;
List<opportunity> oppOld = (List<Opportunity>) oldSo;
}
public void afterDelete(List<SObject> oldSo){ // after delete logic here
List<Opportunity> oppOld = (List<Opportunity>) oldSo;
}
}
Bulk Triggers
- Triggers need to be written to handle multiple records and not exceed governor limits
- Use collections: sets and maps can be used to reduce the number of data operations and act on multiple records at one time
- Bulk operations: bulk triggers can handle bulk operations like data import, bulk API calls, mass actions and recursive Apex methods and triggers
- Avoid limit exceptions: If a trigger is not designed to process thousands of records at once, it may reach the DML statement limit per transaction
- Using DML statements: bulkified triggers should only use one DML statement
- Limit Queries and Results
- SOQL Queries: governor limits exist that enforce a max number of SOQL queries that can be performed in a transaction
- Optimize Query: all necessary data should be retrieved in a single query, the results placed in a collection, and the results iterated over
- Minimize Results: criteria using the
WHERE
clause should always be added to SOQL SELECT statements to filter out unnecessary records
trigger OwnerDesignation on Account (before insert, before update) {
// A set is used here to create unique ownerIds.
Set<Id> ownerIds = newSet<Id>();
for (Account a : Trigger.new) {
ownerIds.add(a.OwnerId);
}
// A map is used here to query for the designations of all the unique ownerIds
// on user records.
Map<Id, User> owners = newMap<Id, User>([SELECT Designation__c
FROM User
WHERE Id IN :ownerIds]);
// Trigger.new is used to iterate over a list of all new account records.
// The Owner_Designation c field on account records is set
// to the designation on user records.
for (Account a : Trigger.new) {
a.Owner_Designation__c = owners.get(a.OwnerId).Designation__c;
}
}
Serialize/Deserialize an Apex Class
@JsonAccess
annotation can be defined on an Apex class and control how it can be serialized and/or deserialized using the following parameter values:never
: serialization/deserialization is never allowedsameNamespace
: can only serialize or deserialize if its in the same namespacesamePackage
: can only serialize or deserialize if its in the same package and impacts only 2nd-generation packagesalways
: serialization or deserialization is always allowed for any Apex code
// Sample class is serializable in the same namespace, and deserializable in the same package
@JsonAccess(serializable='sameNamespace', deserializable='samePackage')
public class SomeSerializableClass {
//..
}
// Sample class is always serializable, and deserializable only in the same namespace
@JsonAccess(serializable='always')
public class AlwaysSerializable {
//..
}
// Sample class is never deserializable, and serializable only in the same namespace
@JsonAccess(deserializable='never')
public class NeverDeserializable {
//..
}
Platform Events
- Publishing Platform Events: Apex can be used to publish platform event messages using the
EventBus.publish
method. In the example below, a platform event is published when an opportunity is updated to Closed WonDatabase.SaveResult
object is returned by theEventBus.publish()
method - it can be used to determine whether event messages were published successfully
trigger OpportunityTrigger on Opportunity (before update) {
List<Opportunity_Event__e> opp_events = newList<Opportunity_Event__e>();
for(Opportunityo :Trigger.new){
if(o.StageName == 'Closed Won') {
Opportunity_Event__eoe = newOpportunity_Event__e();
oe.Stage__c = o.StageName;
opp_events.add(oe);
}
}
List<Database.SaveResult> results = EventBus.publish(opp_events);
for (Database.SaveResultsr : results) {
if (sr.isSuccess()) {
System.debug('Successfully published event.');
} else {
for(Database.Errorerr : sr.getErrors()) {
System.debug('Error returned: ' +
err.getStatusCode() + ' - ' +
err.getMessage());
}
}
}
}
- Subscribing to Platform Events: An Apex trigger can be used to subscribe to a platform event message
- To subscribe an Apex trigger to a platform event, an after insert trigger is created on the Platform Event object, which is the
Order_Event__e
.- Note platform events only support after insert events
- To subscribe an Apex trigger to a platform event, an after insert trigger is created on the Platform Event object, which is the
// trigger for listening to order status events.
trigger OrderEventTrigger on Order_Event__e (after insert) {
List<String> trackingIds = newList<String>();
for (Order_Event__e event : Trigger.New) {
trackingIds.add(event.Tracking_Id__c); // collect tracking ids
}
List<Order__c> orders = [SELECT Id FROM Order__c WHERE Tracking_Id__c IN :trackingIds];
List<Order__c> ordersUpdate = newList<Order__c>();
// Iterate through each published event.
for (Order_Event__e event : Trigger.New) {
for (Order__c order : orders) {
if (event.Tracking_Id__c == order.Tracking_Id__c) {
order.Status__c = event.Status__c;
ordersUpdate.add(order);
}
}
}
update ordersUpdate; // save status updates of the orders
}
- Manage Apex Trigger Subscriptions: Apex trigger subscriptions can be suspended or resumed in Platform Events in Setup.
- Once a suspended subscription is resumed, events published during suspension can still be received
Apex Class & Trigger Best Practices
16 Best Practices
- Use Clean, Concise Code: logic should contain as little code as possible and if possible define methods for code that is reused
- Use Declarative Tools Whenever Possible: if a requirement can be met easily and quickly by using a declarative tool, that should be preferred over Apex
- Use Apex when there is no declarative tool that can meet the requirements
- Use SOQL Query Outside the For Loop: instead of placing a SOQL query inside a for loop to retrive the data, a single query should be used outside the loop, then use a for loop to iterate over the results
- This ensures the Apex does not reach the governor limit for max SOQL queries
- Use Maps for SOQL Queries: instead of using a for loop, a map can be used to obtain records from a SOQL query, which is more efficient. Use the following:
Map<Id,sObject> m = new Map<Id,sObject>(SOQL Query);
- Use DML Statement Outside the For Loop: instead of placing a DML statement inside a for loop to perform operations (insert, update, delete, etc), add the records to be inserted/modified to a list, and use a single DML statement on the list after the execution of the loop
- Bulkify Trigger Code: code defined in an Apex trigger should be able to process a batch of records instead of one record at a time
- Logic that involves database operations should be performed outside for loops
Efficient Example: adds records to a collection variable and performs a single DML statement after the for-loop
trigger OpportunityTrigger on Opportunity (before insert, after insert,
before update, after update,
before delete, after delete) {
if (trigger.isAfter && trigger.isInsert){
List<Contact> newConList = newList<Contact>();
for (Opportunity opp: trigger.New){
if (opp.AdditionalContactLastName__c != NULL) {
Contact newCon = newContact();
newCon.LastName = opp.AdditionalContactLastName__c;
newCon.FirstName = opp.AdditionalContactFirstName__c;
newCon.Email = opp.AdditionalContactEmail__c;
newCon.Accountld = opp.Accountld;
newConList.add(newCon);
}
}
insert newConList;
}
}
Inefficient Example: performs SOQL queries and DML statements inside a for-loop
Trigger InefficientAccountTrigger on Account (before update) {
Account a = Trigger.new[0]; // The trigger is used to process a single record only
// This SOQL query is not optimized to retrieve all the necessary records
List<Contact> contacts = [
SELECT Id
FROM Contact
WHERE AccountId = :a.Id
];
for (Contact c : contacts) {
// SOQL queries should not be performed inside loops
List<Contact> results = [
SELECT Trade_Show_Candidate__c, MailingCity
FROM Contact
WHERE Id = :c.Id
AND MailingCity = 'San Francisco'
LIMIT 1
];
if (!results.IsEmpty()) {
Contact con = results.get(0);
con.Trade_Show_Candidate__c = true;
update con; // A DML statement should not be performed inside loops.
}
}
}
Efficient Example: following has been modified to implement best practices where SOQL Query and DML statement are performed outside the for-loop
trigger EfficientAccountTrigger on Account (before update) {
// An Apex trigger should be designed to handle multiple records
// instead of a single record only.
// SOQL queries should be executed outside loops and optimized
// to retrieve all necessary records.
List<Account> accounts = [
SELECT Id,
(SELECT Id, MailingCity FROM Contacts WHERE MailingCity = 'San Francisco')
FROM Account
WHERE Id IN :Trigger.newMap.keySet()
];
// Use a list to process records in bulk and reduce DML statements to issue
List<Contact> contacts = newList<Contact>();
// 'for' loops can be used to process the entire record collection.
for (Account a : accounts) {
for (Contact con : a.contacts) {
con.Trade_Show_Candidate__c = true;
// The record is added to the collection instead of performing a
// DML statement inside the loop.
contacts.add(con);
}
}
// DML statements should be executed outside
// loops and perform database operations in bulk.
update contacts;
}
- Use a Bulkified Helper Class: a helper class that is designed to process records in bulk should contain the logic of the required operations
- Methods of the helper class can be invoked to perform specific operations in the trigger
- Helper should be written to handle collections of records instead of individual records
- Use Queries and For Loops Efficiently: using multiple SOQL queries to retrieve records of a single object should be avoided if a single query with multiple filters can be used instead
- Relationships can be used to reduce the number of queries required to retrieve the records
- Using multiple for loops to loop through the records should be avoided if a single for loop can be used instead
- Define One Trigger Per Object: if possible, only one trigger should be defined per object to avoid redundancies
- Explicit control over which trigger gets executed first is not possible, and eaach trigger does not get its own governor limits
- Use a SOQL For Loop: if a query returns a large volume of data and causes the transaction to exceed the heap limit, a SOQL for loop should be used to process multiple batches of the resulting records
- When a SOQL query is defined in a for loop definition, the query results are chunked into batches of 200 records
Inefficient Example
// This example shows an inefficient Apex trigger and is
// created for a single type of DML event only, i.e., 'before update'.
trigger InefficientQueryLoopAccountTrigger on Account (before update) {
// These two statements show inefficient use of SOQL
// queries to retrieve records of a single object.
// Also, both these queries are expected to return more than 50,000 records.
List<Opportunity> won = [SELECT Id
FROM Opportunity
WHERE StageName = 'Closed Won'
AND AccountId IN: Trigger.newmap.keySet()];
List<Opportunity> lost = [SELECT Id
FROM Opportunity
WHERE StageName = 'Closed Lost'
AND AccountId IN: Trigger.newmap.keySet()];
List<Opportunity> opportunitiesForUpdate = newList<Opportunity>();
// Two for loops are used here to iterate through two types of
// records, of the same object, which is not an efficient approach.
for(Opportunity o : won) {
// Instead of using a helper class, the logic has been
// defined in the trigger.
o.NextStep = 'Create Order';
opportunitiesForUpdate.add(o);
}
for(Opportunity o : lost) {
o.NextStep = 'Send Feedback Form';
opportunitiesForUpdate.add(o);
}
update opportunitiesForUpdate;
}
Efficient Example - optimized version of the previous Apex trigger
// This example shows an efficient Apex trigger.
// This trigger uses the 'one trigger per object' design
// approach and considers multiple possible DML events.
trigger EfficientQueryLoopAccountTrigger on Account (before insert, after insert,
before update, after update,
before delete) {
if (Trigger.isUpdate) {
// This code statement uses a single query to retrieve
// two types of records of the same object.
List<Opportunity> wonAndLostOpportunities = [SELECT Id, StageName
FROM Opportunity
WHERE AccountId IN: Trigger.newmap.keySet()
AND (StageName = 'Closed Won'
OR StageName = 'Closed Lost')
];
// A single for loop is used here to loop through the retrieved records.
for (Opportunity o : wonAndLostOpportunities) {
// Two if statements are used here to process the two types of records differently.
// A helper class with two static methods has been created for the processing logic.
// The methods of the helper class are being invoked in the trigger here.
if (o.StageName == 'Closed Won') {
HelperClass.helperMethodForWonOpportunities(o);
} else if (o.StageName == 'Closed Lost') {
HelperClass.helperMethodForLostOpportunities(o);
}
}
}
s}
- Use The Limits Apex Methods: methods of the System class called
Limits
should be used to output debug messages for governor limits to determine if or when the code is about to exceed any governor limits- Ex:
Limits.getQueries()
can be used to retrieve the number of SOQL queries that have been used so far Limits.getLimitQueries()
can be used to retrieve the number of SOQL queries allowed
- Ex:
Using the Limits Class
// This example shows how methods of the Limits class can be
// used to obtain information about governor limits.
trigger LimitsAccountTrigger on Account (before insert, after insert,
before update, after update,
before delete, after delete) {
if (Trigger.isBefore && Trigger.isUpdate) {
System.debug('Before SOQL - Total Number of SOQL Queries allowed: ' +
Limits.getLimitQueries());
System.debug('Before SOQL - Total Number of SOQL Queries used: ' +
Limits.getQueries());
List<Opportunity> wonAndLostOpportunities = [
SELECT Id
FROM Opportunity
WHERE AccountId IN: Trigger.newmap.keySet()
AND (StageName = 'Closed Won'
OR StageName = 'Closed Lost')];
System.debug('After SOQL - Total Number of SOQL Queries allowed: ' +
Limits.getLimitQueries());
System.debug('After SOQL - Total Number of SOQL Queries used: ' +
Limits.getQueries());
for (Opportunity o : wonAndLostOpportunities) {
if (o.StageName == 'Closed Won') {
HelperClass.helperMethodForWonOpportunities(o);
} else if (o.StageName == 'Closed Lost') {
HelperClass.helperMethodForLostOpportunities(o);
}
}
}
}
- Use Asynchronous Apex Method Efficiently: when designing triggers to use asynchronous Apex methods, consider governor limits specific to methods with
@future
annotation- No more than 50 @future methods are allowed per Apex invocation
- Do not place an
@future
method in a for loop and ensure it is only invoked once for all records it needs to process
// Example shows how an @future method should be invoked in an Apex trigger
trigger AsyncAccountTrigger on Account (before update) {
// The @future method is invoked only once,
// and a set of all the newly updated records is passed to the method
AsyncApexClass.accountMethod(Trigger.newMap.keySet());
}
- Design Tests to Verify Bulk Operations: Unit tests should be designed to verify that Apex triggers can handle large datasets and not just single records
- Use
Test.startTest
andTest.stopTest
to get a fresh set of governor limits
- Use
// The test class of an Apex trigger should be designed to verify handling of large datasets
@isTest
public class ApexTriggerTest {
@isTest
static void testAccountTrigger() {
// The code below inserts 200 account records to test a 'before insert' trigger
List<Account> accounts = newList<Account>();
for (Integer x = 0; x < 200; x++) {
Account a = newAccount(Name = 'Test' + x + 1);
accounts.add(a);
}
Test.startTest();
insert accounts;
Test.stopTest();
}
}
- Keep Logic Out of Triggers: always delegate trigger logic in an Apex handler class, which allows trigger code to focus on managing the order of when and how logic should be executed.
- If logic needs to be modified, the handler class can be updated without touching the trigger
- Handler class can also potentially be reused in a Visualforce page, custom Lightning component, etc
- Use Relationships to Avoid Queries: if Apex code needs to process child records of a parent record, a subquery can be added to an original query that is used for retrieving the parent records
- This avoids the need to perform another query when processing parent records and reduces the number of queries that are called in a single transaction
- Note the Safe navigation operator:
?
List<Account> accounts = [
SELECT Id, Name,
(SELECT Id, Name, Amount, StageName
FROM Opportunities)
FROM Account
WHERE Id IN :accountIds];
// Loop through the Account records
for (Account a: accounts) {
// The below won't be necessary. Also, SOQL queries should NOT be performed in a loop!
// List<Opportunity> opportunities = [SELECT Id, Name, Amount, Stage
// FROM Opportunities WHERE AccountId = :a.Id];
// Child relationship dot syntax is used to access related opportunities
for (Opportunity o: a.Opportunities){
System.debug('Name: ' + o.Name + ' | Stage: ' + o.StageName + ' | Amount: ' + o.Amount );
}
}
Safe Navigation Operator Examples
// Example 1: Chaining a method
String url = null;
// a) Using a traditional method
if (user.getProfileUrl() != null) {
url = user.getProfileUrl().toExternalForm();
}
// b) Using the safe navigation operator
url = user.getProfileUrl()?toExternalForm();
// Example 2: Chaining an object property/field
// a) Using a traditional method
List<Account> results = new List<Account>();
results = [SELECT Name FROM Account WHERE Id = :accountId];
return results.size() == 0 ? null : results[0].Name;
// b) Using the safe navigation operator
return [SELECT Name FROM Account WHERE Id = :accId]?Name;
- Avoid Hardcoding IDs: there is no guarantee that the Ids will be the same in another environment. If an application with hardcoded record type Ids, for example, is deployed to another org it may not work
- Instead, use the developer name when referring to the record type as this is a user-defined field value that is not generated by the system
Example of Retrieving the Id of a Record Type using the developer name of the record type
public static Id getRecordTypeId(String developerName) {
return (Schema
.SObjectType
.Account
.getRecordTypeInfosByDeveloperName()?
.get(developerName)?
.getRecordTypeId());
}
Scenarios and Solutions
Reference FoF Slides