Apex Best Practices
- Bulkify your Code
- Avoid SOQL Queries or DML statements inside FOR Loops
- Bulkify your Helper Methods
- Using Collections, Streamlining Queries, and Efficient For Loops
- Streamlining Multiple Triggers on the Same Object
- Querying Large Data Sets
- Use of the Limits Apex Methods to Avoid Hitting Governor Limits
- Use
@future
Appropriately
- Writing Test Methods to Verify Large Datasets
- Avoid Hardcoding IDs
1. Bulkify your Code
- Making sure that Apex code can properly handles more than just one record at a time (batch)
- When a batch of object records initiates Apex, a single instance of your Apex code is executed.
This single instance can handle all the records in the given batch.
// NOT Recommended
trigger accountTestTrggr on Account (before insert, before update) {
// This only handles the first record in the Trigger.new collection
// But if more than one Account record initiated this trigger, those additional records
// will not be processed!
Account acct = Trigger.new[0]; // first record
List contacts = [select id, salutation, firstname, lastname, email
from Contact where accountId = :acct.Id];
}
// Recommended
// you can handle all the records in the batch (initiated by this trigger) by:
trigger accountTestTrggr on Account (before insert, before update) {
List accountNames = new List{};
//Loop through all records in the Trigger.new collection
for(Account a: Trigger.new) { // we are now taking care of the all the records in teh collection Trigger.new
//Concatenate the Name and billingState into the Description field
a.Description = a.Name + ':' + a.BillingState
}
}
//======
2. Avoid SOQL Queries or DML statements inside FOR Loops
- Do not place SOQL or DML(insert/update/delete/undelete) statments inside a loop
-
When these operations are placed inside a for loop,
database operations are invoked once per iteration of the loop making it very easy to reach these SFDC governor limits
- Solution: Move SOQL/DML out of loops
- Query: If you need query results, get all the records using a single query and iterate over the resultset
- Update: If you need to update, batch up the data into a collection and invoke DML once for that collection
trigger accountTestTrggr on Account (before insert, before update) {
//For loop to iterate through all the incoming Account records
for(Account a: Trigger.new) {
// Query inside LOOP!
// Since the SOQL Query for related Contacts is within the FOR loop, if this trigger is initiated
// with more than 100 records, the trigger will exceed the trigger governor limit
// of maximum 100 SOQL Queries
// Note: governor limits are calculated at runtime
List contacts = [SELECT id, salutation, firstname, lastname, email
FROM Contact where accountId = :a.Id];
// Update inside a LOOP inside LOOP
for(Contact c: contacts) {
System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '],
LastName[' + c.lastname +']');
c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname;
//Since the UPDATE dml operation is within the FOR loop, if this trigger is initiated
//with more than 150 records, the trigger will exceed the trigger governor limit
//of 150 DML Operations maximum.
update c;
}
}
}
2. Avoid SOQL Queries or DML statements inside FOR Loops - Contd.
// Recommended code
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 accountsWithContacts = [select id, name, (SELECT id, salutation, description,
firstname, lastname, email from Contacts)
FROM Account WHERE Id IN :Trigger.newMap.keySet()];
List contactsToUpdate = new List{};
// 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;
}
//====
4. Bulkify your Helper Methods
- Make sure that the helper methods are properly designed to handle bulk records
- These methods should be written to be invoked with a set of records, especially if the method has a SOQL query or DML operation
- Use the power of the SOQL where clause to query all data needed in a single query
5. Using Collections, Streamlining Queries, and Efficient For Loops
- Use Apex Collections to efficiently query data and store the data in memory
-
A combination of using collections and streamlining SOQL queries can substantially help writing efficient Apex code and avoid governor limits
trigger accountTrigger on Account (before delete, before insert, before update) {
//This code efficiently queries all related Closed Lost and
//Closed Won opportunities in a single query.
List accountWithOpptys = [SELECT id, name, (select id, name, closedate,
stagename FROM Opportunities
WHERE accountId IN :Trigger.newMap.keySet()
AND
(StageName='Closed - Lost' OR StageName = 'Closed - Won'))
FROM Account
WHERE Id IN :Trigger.newMap.keySet()];
//Loop through Accounts only once - governor-friendly
for(Account a : accountWithOpptys){
//Loop through related Opportunities only once
for(Opportunity o: a.Opportunities){
if(o.StageName == 'Closed - Won'){
System.debug('Opportunity Closed Won...do some more logic here...');
}else if(o.StageName =='Closed - Lost'){
System.debug('Opportunity Closed Lost...do some more logic here...');
}
}
}
}
//=====
5. Streamlining Multiple Triggers on the Same Object
-
You do not have any explicit control over which trigger gets initiated first
-
Each trigger that is invoked does not get its own governor limits -
all code that is processed, including the additional triggers, share those available resources.
Instead one trigger getting a maximum of 100 queries, all triggers on that same object will share those 100 queries.
- So, it is critical to ensure that the multiple triggers are efficient and no redundancies exist
6. Querying Large Data Sets
-
The total number of records that can be returned by SOQL queries in a request is 50,000
If returning a large set of queries causes you to exceed your heap limit,
then a SOQL query for loop must be used instead
-
The loop can process multiple batches of records through the use of internal calls to query and queryMore
-
// Use this format for efficiency if you are executing DML statements
// within the for loop. Be careful not to exceed the 150 DML statement limit.
Account[] accts = new Account[];
// loop
// Patform chunk your large query results into batches of 200 records
// by using this syntax where the SOQL query is in the for loop definition,
// and then handle the individual datasets in the for loop logic.
for (List acct : [SELECT id, name FROM account
WHERE name LIKE 'Acme']) {
// Your logic here
accts.add(acct);
}
update accts;
//=====
7. Use of the Limits Apex Methods to Avoid Hitting Governor Limits
- System class Limits - use this to output debug message about each governor limit - 2 version
- First: (e.g.
Limits.getHeapSize()
) returns the amount of the resource that has been used in the current context
- Second: Contains the word limit (e.g.
Limits.getLimitDmlStatements()
) and
returns the total amount of the resource that is available for that context
- Use in the Apex code directly to throw error messages before reaching a governor limit
// Uses IF statement to evaluate if the trigger is about to update too many Opportunities
trigger accountLimitExample on Account (after delete, after insert, after update) {
System.debug('Total Number of SOQL Queries allowed in this Apex code context: ' + Limits.getLimitQueries());
System.debug('Total Number of records that can be queried in this Apex code context: ' + Limits.getLimitDmlRows());
System.debug('Total Number of DML statements allowed in this Apex code context: ' + Limits.getLimitDmlStatements() );
System.debug('Total Number of CPU usage time (in ms) allowed in this Apex code context: ' + Limits.getLimitCpuTime());
// Query the Opportunity object
List opptys =
[SELECT id, description, name, accountid, closedate, stagename
FROM Opportunity
WHERE accountId IN: Trigger.newMap.keySet()];
System.debug('1. Number of Queries used in this Apex code so far: ' + Limits.getQueries());
System.debug('2. Number of rows queried in this Apex code so far: ' + Limits.getDmlRows());
System.debug('3. Number of DML statements used so far: ' + Limits.getDmlStatements());
System.debug('4. Amount of CPU time (in ms) used so far: ' + Limits.getCpuTime());
//NOTE:Proactively determine if there are too many Opportunities to update and avoid governor limits
if (opptys.size() + Limits.getDMLRows() > Limits.getLimitDMLRows()) {
System.debug('Need to stop processing to avoid hitting a governor limit. Too many related Opportunities to update in this trigger');
System.debug('Trying to update ' + opptys.size() + ' opportunities but governor limits will only allow ' + Limits.getLimitDMLRows());
for (Account a : Trigger.new) {
a.addError('You are attempting to update the addresses of too many accounts at once. Please try again with fewer accounts.');
}
}
else{
System.debug('Continue processing. Not going to hit DML governor limits');
System.debug('Going to update ' + opptys.size() + ' opportunities and governor limits will allow ' + Limits.getLimitDMLRows());
for(Account a : Trigger.new){
System.debug('Number of DML statements used so far: ' + Limits.getDmlStatements());
for(Opportunity o: opptys){
if (o.accountid == a.id)
o.description = 'testing';
}
}
update opptys;
System.debug('Final number of DML statements used so far: ' + Limits.getDmlStatements());
System.debug('Final heap size: ' + Limits.getHeapSize());
}
}
//=====
Apex Governor Limit Warning Emails
8. Use @future (asynchronous Apex methods) Appropriately
- Apex written within an asynchronous method gets its own independent set of higher governor limits
- No more than 10 @future methods can be invoked within a single Apex transaction
- 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
@future inefficient code
global class asyncApex {
@future
public static void processAccount(Id accountId) {
// gets single contact
List contacts = [SELECT id, salutation, firstname, lastname, email
FROM Contact
WHERE accountId = :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;
}
}
//====
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);
}
}
//====
@future efficient code
// redesigned to receive a set of records
global class asyncApex {
@future
public static void processAccount(Set accountIds) {
// gets set of records
List contacts = [select id, salutation, firstname, lastname, email
FROM Contact
WHERE accountId IN :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;
}
}
//====
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());
}
//====
9. Writing Test Methods to Verify Large Datasets
- Essential to have test scenarios to verify that the Apex being tested is designed to handle large datasets
and not just single records
-
Apex trigger can be invoked either by a data operation from the user interface or by a data operation from the Force.com SOAP/REST API.
-
- The API can send multiple records per batch,
leading to the trigger being invoked with several records.
Therefore, it is key to have test methods that verify that all Apex code is properly designed to handle larger datasets
and that it does not exceed governor limits.
9. Writing Test Methods to Verify Large Datasets - contd.
// trigger
trigger contactTest on Contact (before insert, before update) {
Set accountIds = new Set();
// form accountIds collection
for(Contact ct: Trigger.new)
accountIds.add(ct.AccountId);
//Do SOQL Query outside the loop
Map accounts = new Map(
[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....
}
}
}
//===
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[]{};
// 200 contacts
for(Integer x = 0; x < 200; x++) {
// contactTest will be fired
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();
}
}
//======
10. Avoid Hardcoding IDs
-
When deploying Apex code between sandbox and production environments, or installing Force.com AppExchange packages
it is essential to avoid hardcoding IDs in the Apex code
- Query for the record types in the code, stores the dataset in a map collection for easy retrieval, and ultimately avoids any hardcoding
- This ensures that code can be deployed safely to different environments
//Query for the Account record types
List 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 accountRecordTypes = new Map{};
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...
}
}
TriggerHandler class
//refer:
// https://raw.githubusercontent.com/kevinohara80/sfdc-trigger-framework/master/src/classes/TriggerHandler.cls
public virtual class TriggerHandler {
// static map of handlername, times run() was invoked
private static Map loopCountMap;
private static Set bypassedHandlers;
// the current context of the trigger, overridable in tests
@TestVisible
private TriggerContext context;
// the current context of the trigger, overridable in tests
@TestVisible
private Boolean isTriggerExecuting;
// static initialization
static {
loopCountMap = new Map();
bypassedHandlers = new Set();
}
// constructor
public TriggerHandler() {
this.setTriggerContext();
}
/***************************************
* public instance methods
***************************************/
// main method that will be called during execution
public void run() {
if(!validateRun()) return;
addToLoopCount();
// dispatch to the correct handler method
if(this.context == TriggerContext.BEFORE_INSERT) {
this.beforeInsert();
} else if(this.context == TriggerContext.BEFORE_UPDATE) {
this.beforeUpdate();
} else if(this.context == TriggerContext.BEFORE_DELETE) {
this.beforeDelete();
} else if(this.context == TriggerContext.AFTER_INSERT) {
this.afterInsert();
} else if(this.context == TriggerContext.AFTER_UPDATE) {
this.afterUpdate();
} else if(this.context == TriggerContext.AFTER_DELETE) {
this.afterDelete();
} else if(this.context == TriggerContext.AFTER_UNDELETE) {
this.afterUndelete();
}
}
public void setMaxLoopCount(Integer max) {
String handlerName = getHandlerName();
if(!TriggerHandler.loopCountMap.containsKey(handlerName)) {
TriggerHandler.loopCountMap.put(handlerName, new LoopCount(max));
} else {
TriggerHandler.loopCountMap.get(handlerName).setMax(max);
}
}
public void clearMaxLoopCount() {
this.setMaxLoopCount(-1);
}
/***************************************
* public static methods
***************************************/
public static void bypass(String handlerName) {
TriggerHandler.bypassedHandlers.add(handlerName);
}
public static void clearBypass(String handlerName) {
TriggerHandler.bypassedHandlers.remove(handlerName);
}
public static Boolean isBypassed(String handlerName) {
return TriggerHandler.bypassedHandlers.contains(handlerName);
}
public static void clearAllBypasses() {
TriggerHandler.bypassedHandlers.clear();
}
/***************************************
* private instancemethods
***************************************/
@TestVisible
private void setTriggerContext() {
this.setTriggerContext(null, false);
}
@TestVisible
private void setTriggerContext(String ctx, Boolean testMode) {
if(!Trigger.isExecuting && !testMode) {
this.isTriggerExecuting = false;
return;
} else {
this.isTriggerExecuting = true;
}
if((Trigger.isExecuting && Trigger.isBefore && Trigger.isInsert) ||
(ctx != null && ctx == 'before insert')) {
this.context = TriggerContext.BEFORE_INSERT;
} else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isUpdate) ||
(ctx != null && ctx == 'before update')){
this.context = TriggerContext.BEFORE_UPDATE;
} else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isDelete) ||
(ctx != null && ctx == 'before delete')) {
this.context = TriggerContext.BEFORE_DELETE;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isInsert) ||
(ctx != null && ctx == 'after insert')) {
this.context = TriggerContext.AFTER_INSERT;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUpdate) ||
(ctx != null && ctx == 'after update')) {
this.context = TriggerContext.AFTER_UPDATE;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isDelete) ||
(ctx != null && ctx == 'after delete')) {
this.context = TriggerContext.AFTER_DELETE;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUndelete) ||
(ctx != null && ctx == 'after undelete')) {
this.context = TriggerContext.AFTER_UNDELETE;
}
}
// increment the loop count
@TestVisible
private void addToLoopCount() {
String handlerName = getHandlerName();
if(TriggerHandler.loopCountMap.containsKey(handlerName)) {
Boolean exceeded = TriggerHandler.loopCountMap.get(handlerName).increment();
if(exceeded) {
Integer max = TriggerHandler.loopCountMap.get(handlerName).max;
throw new TriggerHandlerException('Maximum loop count of ' + String.valueOf(max) + ' reached in ' + handlerName);
}
}
}
// make sure this trigger should continue to run
@TestVisible
private Boolean validateRun() {
if(!this.isTriggerExecuting || this.context == null) {
throw new TriggerHandlerException('Trigger handler called outside of Trigger execution');
}
if(TriggerHandler.bypassedHandlers.contains(getHandlerName())) {
return false;
}
return true;
}
@TestVisible
private String getHandlerName() {
return String.valueOf(this).substring(0,String.valueOf(this).indexOf(':'));
}
/***************************************
* context methods
***************************************/
// context-specific methods for override
@TestVisible
protected virtual void beforeInsert(){}
@TestVisible
protected virtual void beforeUpdate(){}
@TestVisible
protected virtual void beforeDelete(){}
@TestVisible
protected virtual void afterInsert(){}
@TestVisible
protected virtual void afterUpdate(){}
@TestVisible
protected virtual void afterDelete(){}
@TestVisible
protected virtual void afterUndelete(){}
/***************************************
* inner classes
***************************************/
// inner class for managing the loop count per handler
@TestVisible
private class LoopCount {
private Integer max;
private Integer count;
public LoopCount() {
this.max = 5;
this.count = 0;
}
public LoopCount(Integer max) {
this.max = max;
this.count = 0;
}
public Boolean increment() {
this.count++;
return this.exceeded();
}
public Boolean exceeded() {
if(this.max < 0) return false;
if(this.count > this.max) {
return true;
}
return false;
}
public Integer getMax() {
return this.max;
}
public Integer getCount() {
return this.count;
}
public void setMax(Integer max) {
this.max = max;
}
}
// possible trigger contexts
@TestVisible
private enum TriggerContext {
BEFORE_INSERT, BEFORE_UPDATE, BEFORE_DELETE,
AFTER_INSERT, AFTER_UPDATE, AFTER_DELETE,
AFTER_UNDELETE
}
// exception class
public class TriggerHandlerException extends Exception {}
}
Apex Trigger Common Issues
- Trigger logic is becoming hard to understand and maintain
- Slight change required might mean complete rewrite of the trigger
- Hard to test
- Trigger logic is executing out-of-order
- Governor Limits are hit
Trigger - Best Practice - 1 - One Trigger Per Object
- If we develop multiple Triggers for a single object,
we have no way of controlling the order of execution
-
Best Practice: A single Trigger can handle all possible combinations of Trigger contexts
// A single Trigger can handle all possible combinations of Trigger contexts
trigger OpportunityTrigger on Opportunity (
before insert, before update, before delete,
after insert, after update, after delete, after undelete) {
// trigger body
}
Trigger - Best Practice - 2 - Logic-Less Trigger
Create context-specific handler methods in Trigger handlers
trigger OpportunityTrigger on Opportunity (after insert, after update) {
// look at the context that the trigger is running?
// after insert
if(Trigger.isAfter && Trigger.isInsert) {
OpportunityTriggerHandler.handleAfterInsert(Trigger.new);
} else if(Trigger.isAfter && Trigger.isUpdate) { // after update
OpportunityTriggerHandler.handleAfterUpdate(Trigger.new, Trigger.old);
}
}
Benefits of using Trigger Framework
- Help you to conform to best practices
- Make implementing new logic and new context handlers very easy
- Simplify testing and maintenance of your application logic
- Enforces consistent implementation of Trigger logic
- Implement tools, utilities, and abstractions to make your handler logic as lightweight as possible
Key features of a Trigger Framework
- Routing Abstractions: Look at the context that the trigger is running and dispatch to the correct handler method accordingly
- Recursion Detection and Prevention: Detect this and figure out how to handle the situation properly
- Centralize Switch (Enable/Disable) of Triggers:
Easily be wired up to a Custom Setting in the org to give us on/off control of the triggers
Basic Implemention of Trigger Framework
// TRIGGER
trigger OpportunityTrigger on Opportunity (after insert, after update, after delete, after undelete) {
// one-liner for taking care of all 4 contexts
new OpportunityTriggerHandler().run();
}
// HANDLER - OpportunityTriggerHandler inherts the base case:
// TriggerHandler - the base class provided by the framework
public class OpportunityTriggerHandler extends TriggerHandler {
// CONSTRUCTOR
public OpportunityTriggerHandler() {
// prevent a single TriggerHandler from firing recursively
// sets the number of executions per Trigger:
this.setMaxLoopCount(1);
}
// override default with a custom function
protected void override beforeUpdate() {
setLostOpptysToZero(); // call the private methods
}
// other contexts overrides if needed
protected void override afterInsert() {
doSomeAfterInsertStuff();
}
protected void override beforeDelete() {
doSomeStuffBeforeDelete();
}
// PRIVATE METHODS
// update the Amount field to zero when an opportunity is marked Closed/Lost
private void setLostOpptysToZero(List) {
// NOTE: Trigger.new and Trigger.newMap always contain raw sObjects or Maps of sObjects respectively.
for(Opportunity oppty : (List) Trigger.new) {
if(oppty.StageName == 'Closed Lost' && oppty.Amount > 0) {
oppty.Amount = 0;
}
}
}
}
//======
run() method in Trigger Framework
// Checks the current context for the trigger
// Calls it’s own handler method for it
public void run() {
if(!validateRun()) return;
addToLoopCount(); // Recursion Protection
// dispatch to the correct handler method based on the context
if(Trigger.isBefore && Trigger.isInsert) {
// overridable: defined logic-less and meant to be overridden
// If they aren’t overridden, nothing really happens
// defined as: protected virtual void beforeInsert(){ }
this.beforeInsert();
} else if(Trigger.isBefore && Trigger.isUpdate) {
this.beforeUpdate();
} else if(Trigger.isBefore && Trigger.isDelete) {
this.beforeDelete();
} else if(Trigger.isAfter && Trigger.isInsert) {
this.afterInsert();
} else if(Trigger.isAfter && Trigger.isUpdate) {
this.afterUpdate();
} else if(Trigger.isAfter && Trigger.isDelete) {
this.afterDelete();
} else if(Trigger.isAfter && Trigger.isUndelete) {
this.afterUndelete();
}
}
Source: TriggerHandler class
//refer:
// https://raw.githubusercontent.com/kevinohara80/sfdc-trigger-framework/master/src/classes/TriggerHandler.cls
public virtual class TriggerHandler {
// static map of handlername, times run() was invoked
private static Map loopCountMap;
private static Set bypassedHandlers;
// the current context of the trigger, overridable in tests
@TestVisible
private TriggerContext context;
// the current context of the trigger, overridable in tests
@TestVisible
private Boolean isTriggerExecuting;
// static initialization
static {
loopCountMap = new Map();
bypassedHandlers = new Set();
}
// constructor
public TriggerHandler() {
this.setTriggerContext();
}
/***************************************
* public instance methods
***************************************/
// main method that will be called during execution
public void run() {
if(!validateRun()) return;
addToLoopCount();
// dispatch to the correct handler method
if(this.context == TriggerContext.BEFORE_INSERT) {
this.beforeInsert();
} else if(this.context == TriggerContext.BEFORE_UPDATE) {
this.beforeUpdate();
} else if(this.context == TriggerContext.BEFORE_DELETE) {
this.beforeDelete();
} else if(this.context == TriggerContext.AFTER_INSERT) {
this.afterInsert();
} else if(this.context == TriggerContext.AFTER_UPDATE) {
this.afterUpdate();
} else if(this.context == TriggerContext.AFTER_DELETE) {
this.afterDelete();
} else if(this.context == TriggerContext.AFTER_UNDELETE) {
this.afterUndelete();
}
}
public void setMaxLoopCount(Integer max) {
String handlerName = getHandlerName();
if(!TriggerHandler.loopCountMap.containsKey(handlerName)) {
TriggerHandler.loopCountMap.put(handlerName, new LoopCount(max));
} else {
TriggerHandler.loopCountMap.get(handlerName).setMax(max);
}
}
public void clearMaxLoopCount() {
this.setMaxLoopCount(-1);
}
/***************************************
* public static methods
***************************************/
public static void bypass(String handlerName) {
TriggerHandler.bypassedHandlers.add(handlerName);
}
public static void clearBypass(String handlerName) {
TriggerHandler.bypassedHandlers.remove(handlerName);
}
public static Boolean isBypassed(String handlerName) {
return TriggerHandler.bypassedHandlers.contains(handlerName);
}
public static void clearAllBypasses() {
TriggerHandler.bypassedHandlers.clear();
}
/***************************************
* private instancemethods
***************************************/
@TestVisible
private void setTriggerContext() {
this.setTriggerContext(null, false);
}
@TestVisible
private void setTriggerContext(String ctx, Boolean testMode) {
if(!Trigger.isExecuting && !testMode) {
this.isTriggerExecuting = false;
return;
} else {
this.isTriggerExecuting = true;
}
if((Trigger.isExecuting && Trigger.isBefore && Trigger.isInsert) ||
(ctx != null && ctx == 'before insert')) {
this.context = TriggerContext.BEFORE_INSERT;
} else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isUpdate) ||
(ctx != null && ctx == 'before update')){
this.context = TriggerContext.BEFORE_UPDATE;
} else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isDelete) ||
(ctx != null && ctx == 'before delete')) {
this.context = TriggerContext.BEFORE_DELETE;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isInsert) ||
(ctx != null && ctx == 'after insert')) {
this.context = TriggerContext.AFTER_INSERT;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUpdate) ||
(ctx != null && ctx == 'after update')) {
this.context = TriggerContext.AFTER_UPDATE;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isDelete) ||
(ctx != null && ctx == 'after delete')) {
this.context = TriggerContext.AFTER_DELETE;
} else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUndelete) ||
(ctx != null && ctx == 'after undelete')) {
this.context = TriggerContext.AFTER_UNDELETE;
}
}
// increment the loop count
@TestVisible
private void addToLoopCount() {
String handlerName = getHandlerName();
if(TriggerHandler.loopCountMap.containsKey(handlerName)) {
Boolean exceeded = TriggerHandler.loopCountMap.get(handlerName).increment();
if(exceeded) {
Integer max = TriggerHandler.loopCountMap.get(handlerName).max;
throw new TriggerHandlerException('Maximum loop count of ' + String.valueOf(max) + ' reached in ' + handlerName);
}
}
}
// make sure this trigger should continue to run
@TestVisible
private Boolean validateRun() {
if(!this.isTriggerExecuting || this.context == null) {
throw new TriggerHandlerException('Trigger handler called outside of Trigger execution');
}
if(TriggerHandler.bypassedHandlers.contains(getHandlerName())) {
return false;
}
return true;
}
@TestVisible
private String getHandlerName() {
return String.valueOf(this).substring(0,String.valueOf(this).indexOf(':'));
}
/***************************************
* context methods
***************************************/
// context-specific methods for override
@TestVisible
protected virtual void beforeInsert(){}
@TestVisible
protected virtual void beforeUpdate(){}
@TestVisible
protected virtual void beforeDelete(){}
@TestVisible
protected virtual void afterInsert(){}
@TestVisible
protected virtual void afterUpdate(){}
@TestVisible
protected virtual void afterDelete(){}
@TestVisible
protected virtual void afterUndelete(){}
/***************************************
* inner classes
***************************************/
// inner class for managing the loop count per handler
@TestVisible
private class LoopCount {
private Integer max;
private Integer count;
public LoopCount() {
this.max = 5;
this.count = 0;
}
public LoopCount(Integer max) {
this.max = max;
this.count = 0;
}
public Boolean increment() {
this.count++;
return this.exceeded();
}
public Boolean exceeded() {
if(this.max < 0) return false;
if(this.count > this.max) {
return true;
}
return false;
}
public Integer getMax() {
return this.max;
}
public Integer getCount() {
return this.count;
}
public void setMax(Integer max) {
this.max = max;
}
}
// possible trigger contexts
@TestVisible
private enum TriggerContext {
BEFORE_INSERT, BEFORE_UPDATE, BEFORE_DELETE,
AFTER_INSERT, AFTER_UPDATE, AFTER_DELETE,
AFTER_UNDELETE
}
// exception class
public class TriggerHandlerException extends Exception {}
}
System.Debug(...) in PROD
- Has effects on
- Log sizes
- CPU time - depending on evaluation of the expressions used in the
System.Debug(...)
method
- Can count against Governor Limits - especially CPU time
- May result in timeout based on evaluation of the expressions used in the Debug method
- Best Practice:
- Security review of the code going to PROD will demand removing of
System.Debug(...)
going to PROD
- Code-Scanners like PMD will complain about this issue
- If proper Test Methods are written, you need for
System.Debug(...)
will be very low
- Use a Custom Setting flag say
isDev
to provide switch on/off for System.Debug(...)
,
this may involve a CPU time spent on evaluation of this flag
- Refer: Debug Log Levels