# Thursday, 08 July 2010

No, not this Trigger... keep reading...

Trigger development (apologies to Roy Rogers' horse) is not done on a daily basis by a typical Force.com Developer.

In my case, Trigger development is similar to using regular expressions (regex) in that I often rely on documentation and previously developed code examples to refresh my memory, do the coding, then put it aside for several weeks/months.

I decided to create a more fluent Trigger template to address the following challenges and prevent me from repeatedly making the same mistakes:

  • Bulkification best practices not provisioned by the Trigger creation wizard
  • Use of the 7 boolean context variables in code (isInsert, isBefore, etc...) greatly impairs readability and long-term maintainability
  • Trigger.old and Trigger.new collections are not available in certain contexts
  • Asynchronous trigger support not natively built-in

The solution was to create a mega-Trigger that handles all events and delegates them accordingly to an Apex trigger handler class.

You may want to customize this template to your own style. Here are some design considerations and assumptions in this template:

  • Use of traditional event method names on the handler class (OnBeforeInsert, OnAfterInsert)
  • Maps are used where they are most relevant
  • Objects in map collections cannot be modified, however there is nothing in the compiler to prevent you from trying. Remove them whenever not needed.
  • Maps are most useful when triggers modify other records by IDs, so they're included in update and delete triggers
  • Encourage use of asynchronous trigger processing by providing pre-built @future methods.
  • @future methods only support collections of native types. ID is preferred using this style.
  • Avoid use of before/after if not relevant. Example: OnUndelete is simpler than OnAfterUndelete (before undelete is not supported)
  • Provide boolean properties for determining trigger context (Is it a Trigger or VF/WebService call?)
  • There are no return values. Handler methods are assumed to assert validation rules using addError() to prevent commit.

References:
Apex Developers Guide - Triggers
Steve Anderson - Two interesting ways to architect Apex triggers

AccountTrigger.trigger

trigger AccountTrigger on Account (after delete, after insert, after undelete, 
after update, before delete, before insert, before update) {
	AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size);
	
	if(Trigger.isInsert && Trigger.isBefore){
		handler.OnBeforeInsert(Trigger.new);
	}
	else if(Trigger.isInsert && Trigger.isAfter){
		handler.OnAfterInsert(Trigger.new);
		AccountTriggerHandler.OnAfterInsertAsync(Trigger.newMap.keySet());
	}
	
	else if(Trigger.isUpdate && Trigger.isBefore){
		handler.OnBeforeUpdate(Trigger.old, Trigger.new, Trigger.newMap);
	}
	else if(Trigger.isUpdate && Trigger.isAfter){
		handler.OnAfterUpdate(Trigger.old, Trigger.new, Trigger.newMap);
		AccountTriggerHandler.OnAfterUpdateAsync(Trigger.newMap.keySet());
	}
	
	else if(Trigger.isDelete && Trigger.isBefore){
		handler.OnBeforeDelete(Trigger.old, Trigger.oldMap);
	}
	else if(Trigger.isDelete && Trigger.isAfter){
		handler.OnAfterDelete(Trigger.old, Trigger.oldMap);
		AccountTriggerHandler.OnAfterDeleteAsync(Trigger.oldMap.keySet());
	}
	
	else if(Trigger.isUnDelete){
		handler.OnUndelete(Trigger.new);	
	}
}

AccountTriggerHandler.cls

 
public with sharing class AccountTriggerHandler {
	private boolean m_isExecuting = false;
	private integer BatchSize = 0;
	
	public AccountTriggerHandler(boolean isExecuting, integer size){
		m_isExecuting = isExecuting;
		BatchSize = size;
	}
		
	public void OnBeforeInsert(Account[] newAccounts){
		//Example usage
		for(Account newAccount : newAccounts){
			if(newAccount.AnnualRevenue == null){
				newAccount.AnnualRevenue.addError('Missing annual revenue');
			}
		}
	}
	
	public void OnAfterInsert(Account[] newAccounts){
		
	}
	
	@future public static void OnAfterInsertAsync(Set<ID> newAccountIDs){
		//Example usage
		List<Account> newAccounts = [select Id, Name from Account where Id IN :newAccountIDs];
	}
	
	public void OnBeforeUpdate(Account[] oldAccounts, Account[] updatedAccounts, Map<ID, Account> accountMap){
		//Example Map usage
		Map<ID, Contact> contacts = new Map<ID, Contact>( [select Id, FirstName, LastName, Email from Contact where AccountId IN :accountMap.keySet()] );
	}
	
	public void OnAfterUpdate(Account[] oldAccounts, Account[] updatedAccounts, Map<ID, Account> accountMap){
		
	}
	
	@future public static void OnAfterUpdateAsync(Set<ID> updatedAccountIDs){
		List<Account> updatedAccounts = [select Id, Name from Account where Id IN :updatedAccountIDs];
	}
	
	public void OnBeforeDelete(Account[] accountsToDelete, Map<ID, Account> accountMap){
		
	}
	
	public void OnAfterDelete(Account[] deletedAccounts, Map<ID, Account> accountMap){
		
	}
	
	@future public static void OnAfterDeleteAsync(Set<ID> deletedAccountIDs){
		
	}
	
	public void OnUndelete(Account[] restoredAccounts){
		
	}
	
	public boolean IsTriggerContext{
		get{ return m_isExecuting;}
	}
	
	public boolean IsVisualforcePageContext{
		get{ return !IsTriggerContext;}
	}
	
	public boolean IsWebServiceContext{
		get{ return !IsTriggerContext;}
	}
	
	public boolean IsExecuteAnonymousContext{
		get{ return !IsTriggerContext;}
	}
}
Thursday, 08 July 2010 14:29:35 (Pacific Daylight Time, UTC-07:00)
Have you considered making your template class a virtual class handling a generic SObject that then could be extended by any implementation?
Thursday, 08 July 2010 14:33:25 (Pacific Daylight Time, UTC-07:00)
Have you been able to make sObjects work?

The Trigger.new/Trigger.old collections are strongly typed, otherwise more abstract patterns would definitely be preferred.
Mike Leach
Tuesday, 24 August 2010 04:50:40 (Pacific Daylight Time, UTC-07:00)
Hi,

First thank you for this Trigger pattern as I find it really useful.
Question: What would you consider Best practices around Future triggers and when should they NOT be used?
Indeed I noticed that it is possible to use both when it is an after Trigger but is there any rule you would apply to choose which one to use?
Thanks,
Ldel
Ldel
Tuesday, 24 August 2010 16:26:55 (Pacific Daylight Time, UTC-07:00)
Good question Ldel.

The user experience can be improved with faster page responses if long running tasks are executed asynchronously using @future.

Another common use is updating a backend system, such as Financials or ERP, upon successful insert or update of SForce records.

Any other trigger that doesn't do validation or "vote" on the transaction is a potential candidate for @future.

-Mike
Mike Leach
Thursday, 26 August 2010 17:24:20 (Pacific Daylight Time, UTC-07:00)
This is super.

I made a tweak in my code: For the OnBeforeUpdate and OnAfterUpdate methods, I now pass oldMap and newMap only. In the class, I can then recreate Trigger.New by calling oldMap.values(). That also lets me compare the before and after versions of a given record.

What do you think?

David
Sunday, 10 October 2010 12:09:04 (Pacific Daylight Time, UTC-07:00)
This contains very valuable information for everyone who's into writing triggers and especially newcomers.

In my triggers I prefer to iterate over all the records that come into the trigger first and evaluate some criteria to see whether I need to process the record or not and put the records which are about to process in a list which then is handed into the class to perform some real action.
Tuesday, 09 November 2010 04:02:05 (Pacific Standard Time, UTC-08:00)
This is a great template - thanks for posting!
Thursday, 09 December 2010 06:33:02 (Pacific Standard Time, UTC-08:00)
Great template - incorporating template into all my work now

I'm fairly new this, but have one thing I am struggling with. That is how to get full test coverage for the lines below -



public boolean IsTriggerContext{
get{ return m_isExecuting;}
}


public boolean IsVisualforcePageContext{
get{ return !IsTriggerContext;}
}

public boolean IsWebServiceContext{
get{ return !IsTriggerContext;}
}

public boolean IsExecuteAnonymousContext{
get{ return !IsTriggerContext;}
}

Thanks again for the post.
Thursday, 09 December 2010 07:00:41 (Pacific Standard Time, UTC-08:00)
You could possibly try:

system.assert(trigger.IsTriggerContext != null);

or simply remove the unused context booleans.
Mike Leach
Wednesday, 13 April 2011 13:44:50 (Pacific Daylight Time, UTC-07:00)
Hey Mike-

I refer to this at least once a week, thanks for posting it. I found an addition to prevent the trigger from running twice that I think would be helpful to be added to the template. What do you think?

Ray
Wednesday, 13 April 2011 16:04:06 (Pacific Daylight Time, UTC-07:00)
@Ray That's a great addition to the template. Thanks for sharing!
Mike Leach
Comments are closed.