Practice Motivation
These patterns provide an elegant way of coding triggers to avoid bad practices such as repetitive SOQL queries that can hit governor limits and multiple triggers over the same object. The patterns enforce a logical sequence to the trigger code and in turn help to keep code tidy and more maintainable. Keeping trigger logic for each object in a single place avoids problems where multiple triggers are in contention with each other and makes it easier to debug. Enforcing up front caching of data within a trigger keeps the trigger 'bulkified' and avoids those nasty 'too many SOQL queries'.
Description
This pattern involves delegating work from the trigger to a structured Trigger Handler class. Each object will have it's own trigger handler. The trigger itself has almost no code in it. We make use of a Factory class to instantiate the appropriate Trigger Handler. Whenever we create a new trigger all we need to do is create a new Handler Class, add a few lines of code to the Factory class to register the correct handler and add a line of code to the trigger itself to delegate the work.
We start by defining an Interface that provides the template for our trigger handlers. This interface contains the method signatures each handler must implement. See below:
/**
* Interface containing methods Trigger Handlers must implement to enforce best practice
* and bulkification of triggers.
*/
public interface ITrigger
{
/**
* bulkBefore
*
* This method is called prior to execution of a BEFORE trigger. Use this to cache
* any data required into maps prior execution of the trigger.
*/
void bulkBefore();
/**
* bulkAfter
*
* This method is called prior to execution of an AFTER trigger. Use this to cache
* any data required into maps prior execution of the trigger.
*/
void bulkAfter();
/**
* beforeInsert
*
* This method is called iteratively for each record to be inserted during a BEFORE
* trigger. Never execute any SOQL/SOSL etc in this and other iterative methods.
*/
void beforeInsert(SObject so);
/**
* beforeUpdate
*
* This method is called iteratively for each record to be updated during a BEFORE
* trigger.
*/
void beforeUpdate(SObject oldSo, SObject so);
/**
* beforeDelete
*
* This method is called iteratively for each record to be deleted during a BEFORE
* trigger.
*/
void beforeDelete(SObject so);
/**
* afterInsert
*
* This method is called iteratively for each record inserted during an AFTER
* trigger. Always put field validation in the 'After' methods in case another trigger
* has modified any values. The record is 'read only' by this point.
*/
void afterInsert(SObject so);
/**
* afterUpdate
*
* This method is called iteratively for each record updated during an AFTER
* trigger.
*/
void afterUpdate(SObject oldSo, SObject so);
/**
* afterDelete
*
* This method is called iteratively for each record deleted during an AFTER
* trigger.
*/
void afterDelete(SObject so);
/**
* andFinally
*
* This method is called once all records have been processed by the trigger. Use this
* method to accomplish any final operations such as creation or updates of other records.
*/
void andFinally();
}
We can now add a handler class that implements this interface. In the example below we have a handler for the Account object that has some logic to make sure the Account isn't referenced elsewhere before it can be deleted. It also writes a record away to a custom object called Audit__c for each Account deleted. (Supporting classes and details of the Audit__c object will described at the end)
In the example we make use of the bulkBefore method to cache all the in use Account Id's passed to the trigger in a Set. As this method is only called once we will not run the overhead of multiple SOQL queries. The validation is done in the beforeDelete method. This method is called iteratively for every record passed to the before delete trigger. If we were validating field data we would do this in one of the after methods since the values could be modified by another trigger or workflow. If the validation succeeds we add an Audit__c record to a list for handling later in the andFinally method. The andFinally method is executed once at the end of the trigger. In this case we use it to insert the Audit__c records.
You will also notice there is no SOQL in the class, this is delegated out to a Gateway class.
/**
* Class AccountHandler
*
* Trigger Handler for the Account SObject. This class implements the ITrigger
* interface to help ensure the trigger code is bulkified and all in one place.
*/
public with sharing class AccountHandler
implements ITrigger
{
// Member variable to hold the Id's of Accounts 'in use'
private Set<Id> m_inUseIds = new Set<Id>();
// Member variable to record Audit records
private List<Audit__c> m_audits = new List<Audit__c>();
// Constructor
public AccountHandler()
{
}
/**
* bulkBefore
*
* This method is called prior to execution of a BEFORE trigger. Use this to cache
* any data required into maps prior execution of the trigger.
*/
public void bulkBefore()
{
// If this a delete trigger Cache a list of Account Id's that are 'in use'
if (Trigger.isDelete)
{
// pre load all the in use projects passed to this trigger
m_inUseIds = AccountGateway.findAccountIdsInUse(Trigger.oldMap.keySet());
}
}
public void bulkAfter()
{
}
public void beforeInsert(SObject so)
{
}
public void beforeUpdate(SObject oldSo, SObject so)
{
}
/**
* beforeDelete
*
* This method is called iteratively for each record to be deleted during a BEFORE
* trigger.
*/
public void beforeDelete(SObject so)
{
// Cast the SObject to an Account
Account myAccount = (Account)so;
// Examine the Set and if the account is in use don't allow it to be deleted.
if (m_inUseIds.contains(myAccount.Id))
{
// Add the error to the offending object
so.addError('You cannot delete an Account that is in use.');
}
else
{
// Add an audit record to the list
Audit__c myAudit = new Audit__c();
myAudit.Description__c = 'Account Name: ' + myAccount.Name + ' (Id: ' + myAccount.Id + ') was deleted';
m_audits.add(myAudit);
}
}
public void afterInsert(SObject so)
{
}
public void afterUpdate(SObject oldSo, SObject so)
{
}
public void afterDelete(SObject so)
{
}
/**
* andFinally
*
* This method is called once all records have been processed by the trigger. Use this
* method to accomplish any final operations such as creation or updates of other records.
*/
public void andFinally()
{
// insert any audit records
if (!m_audits.isEmpty())
{
insert m_audits;
}
}
}
The factory class below selects the correct handler and executes the methods defined by the interface. It does this by the Object type passed to the static method createHandler. Adding additional handlers requires extending the if statement in the getHandler method.
You will see from the execute method the order in which the interface methods are called and you will note the methods that are called iteratively passing in the relevant SObjects.
/**
* Class TriggerFactory
*
* Used to instantiate and execute Trigger Handlers associated with sObjects.
*/
public with sharing class TriggerFactory
{
/**
* Public static method to create and execute a trigger handler
*
* Arguments: Schema.sObjectType soType - Object type to process (SObject.sObjectType)
*
* Throws a TriggerException if no handler has been coded.
*/
public static void createHandler(Schema.sObjectType soType)
{
// Get a handler appropriate to the object being processed
ITrigger handler = getHandler(soType);
// Make sure we have a handler registered, new handlers must be registered in the getHandler method.
if (handler == null)
{
throw new TriggerException('No Trigger Handler registered for Object Type: ' + soType);
}
// Execute the handler to fulfil the trigger
execute(handler);
}
/**
* private static method to control the execution of the handler
*
* Arguments: ITrigger handler - A Trigger Handler to execute
*/
private static void execute(ITrigger handler)
{
// Before Trigger
if (Trigger.isBefore)
{
// Call the bulk before to handle any caching of data and enable bulkification
handler.bulkBefore();
// Iterate through the records to be deleted passing them to the handler.
if (Trigger.isDelete)
{
for (SObject so : Trigger.old)
{
handler.beforeDelete(so);
}
}
// Iterate through the records to be inserted passing them to the handler.
else if (Trigger.isInsert)
{
for (SObject so : Trigger.new)
{
handler.beforeInsert(so);
}
}
// Iterate through the records to be updated passing them to the handler.
else if (Trigger.isUpdate)
{
for (SObject so : Trigger.old)
{
handler.beforeUpdate(so, Trigger.newMap.get(so.Id));
}
}
}
else
{
// Call the bulk after to handle any caching of data and enable bulkification
handler.bulkAfter();
// Iterate through the records deleted passing them to the handler.
if (Trigger.isDelete)
{
for (SObject so : Trigger.old)
{
handler.afterDelete(so);
}
}
// Iterate through the records inserted passing them to the handler.
else if (Trigger.isInsert)
{
for (SObject so : Trigger.new)
{
handler.afterInsert(so);
}
}
// Iterate through the records updated passing them to the handler.
else if (Trigger.isUpdate)
{
for (SObject so : Trigger.old)
{
handler.afterUpdate(so, Trigger.newMap.get(so.Id));
}
}
}
// Perform any post processing
handler.andFinally();
}
/**
* private static method to get the appropriate handler for the object type.
* Modify this method to add any additional handlers.
*
* Arguments: Schema.sObjectType soType - Object type tolocate (SObject.sObjectType)
*
* Returns: ITrigger - A trigger handler if one exists or null.
*/
private static ITrigger getHandler(Schema.sObjectType soType)
{
if (soType == Account.sObjectType)
{
return new AccountHandler();
}
return null;
}
}
Now we need to wire the trigger up to the TriggerFactory class. This is easily accomplished with a single line of code in the trigger below. The trigger simply passes the object type to the factory method. You will notice that the trigger handles all CRUD operations. I havn't included undelete but you could adapt the pattern to include this.
trigger AccountTrigger on Account (after delete, after insert, after update, before delete, before insert, before update)
{
TriggerFactory.createHandler(Account.sObjectType);
}
The example requires the additional classes below:
public class TriggerException extends Exception {}
/**
* Class AccountGateway
*
* Provides finder methods for accessing data in the Account object.
*/
public without sharing class AccountGateway
{
/**
* Returns a subset of id's where there are any records in use.
*
* Arguments: Set<Id> accIds - Set of Account Id's to examine
*
* Returns: Set<Id> - Set of Account Id's that are 'in use'
*/
public static Set<Id> findAccountIdsInUse(Set<Id> accIds)
{
Set<Id> inUseIds = new Set<Id>();
for (Account[] accounts : [Select p.Id, (Select Id From Opportunities Limit 1) From Account p where p.Id in : accIds])
{
for (Account acc : accounts)
{
if (acc.Opportunities.size() > 0)
{
inUseIds.add(acc.id);
}
}
}
return inUseIds;
}
}
The Audit_c object is a simple custom object with an Autonumber Name field and a single text area field called Description__c.
Discussion
This trigger pattern should work in most situations. As with all triggers care should be taken.
- Always add field validation to the after methods.
- If you need data to persist across the before and after trigger consider the use of static variables.
- Don't put SOQL inside loops or any of the before and after methods.