General trigger bulkification - best practices
Good question, but there are MANY possible answers, so I will just throw in my 2 cents.
The first and easiest way to 'BULKIFY' is to leverage collections in order to save yourself SOQL calls and DML statements.
Here's an older, but still great resource by Jeff Douglass on utilizing collections in Salesforce.
http://blog.jeffdouglas.com/2011/01/06/fun-with-salesforce-collections/
IMO, I would say that leveraging collections is the first and best place to start in trying to optimize and bulkify your triggers. I will now try to show a few examples of how leveraging collections can save you many Governor limit headaches.
This code uses one DML statement for each Account in trigger.new
Trigger myTrigger on Account(after insert) {
for(Account a : trigger.new){
My_Custom_Object__c obj = new My_Custom_Object__c(Account__c = a.Id);
insert obj;
}
}
The example above makes a DML call for every account in trigger.new. If this is a mass insert, you will run into governor limit issues.
This code now uses one DML statement total, regardless of the size of trigger.new
Trigger myTrigger on Account(after insert) {
list<My_Custom_Object__c> objList = new list<My_Custom_Object__c>();
for(Account a : trigger.new){
objList.add(new My_Custom_Object__c(Account__c = a.Id));
}
insert objList;
}
This example moves the DML outside of the loop. Instead you add a new custom object to the list inside of the loop. Once you have gone through the entire list of trigger.new, you insert the list of custom objects.
This code uses one SOQL query for each Account in trigger.new
Trigger myTrigger on Contact(before insert) {
for(Contact c : trigger.new){
if(c.AccountId != null) {
Account a = [Select Id, Name, ShippingCity From Account Where Id =: c.AccountId];
c.ShippingCity = a.ShippingCity;
}
}
}
The example above makes a SOQL query for every contact in trigger.new. If this is a mass insert, you will run into governor limit issues.
This code now uses one SOQL query total, regardless of the size of trigger.new
Trigger myTrigger on Contact(before insert) {
map<Id,Account> accountMap = new map<Id,Account>();
for(Contact c : trigger.new){
accountMap.put(c.AccountId, null);
}
accountMap.remove(null);
accountMap.putAll([Select Id, Name, ShippingCity From Account Where Id In : accountMap.keyset()]);
for(Contact c : trigger.new){
if(accountMap.containsKey(c.AccountId)){
c.ShippingCity = accountMap.get(c.AccountId).ShippingCity;
}
}
}
This example above utilizes a map to store all accounts related to the contacts in trigger.new. The advantage here is that one single SOQL query gathers all the accounts. You can then get the account easily within the loop without have to query the database. You now have the same trigger with a sinlge SOQL query regardless of the size of trigger.new
I believe this is one of the best practices to optimize your triggers for bulk operations.
To take it a step further, there are a few more things that we can do to optimize our triggers. One of the best practices is to only use one trigger per object.
Lets assume you have two specific pieces of business logic that you need to apply after an account is created. The easy way to accomplish this would be to create 2 triggers on the account object.
Trigger myTrigger1 on Contact(after insert) {
//YOUR LOGIC FOR TRIGGER 1
}
Trigger myTrigger2 on Contact(after insert) {
//YOUR LOGIC FOR TRIGGER 2
}
This could work well depending on your situation. What if you have logic in trigger2 that is dependent on the outcomes of trigger1? There is no guarantee the order in which your triggers will run, so in some cases trigger1 will run first and in others trigger2 will run first.
A simple approach to solve this is to combine the logic into a single trigger
Trigger myTrigger1 on Contact(after insert) {
//YOUR FIRST PIECE OF LOGIC
//YOUR SECOND PIECE OF LOGIC
}
This works technically, as you can now control the order of the operations, and it is a best practice to have only 1 trigger per object, but still can be improved a bit. Lets say for arguments sake this is a fairly large trigger, with a few different pieces of complex logic.
Trigger myTrigger1 on Contact(after insert) {
//YOUR FIRST PIECE OF LOGIC
//LOTS OF CODE
//YOUR SECOND PIECE OF LOGIC
//LOTS OF CODE
//YOUR THIRD PIECE OF LOGIC
//LOTS OF CODE
//YOUR N+1 PIECE OF LOGIC
//LOTS OF CODE
}
There are a few things that jump out that might be a problem.
- All this logic is buried in a trigger and is not reusable.
- It is very tough to test a specific piece of logic in the trigger. You basically have to call a DML statement to trigger the entire trigger.
So how do we fix that?
We would want to move the logic from the trigger itself into a utility or handler class.
Trigger ContactTrigger on Contact(before insert, after insert, before update, after update) {
if(trigger.isBefore){
if(trigger.isInsert){
ContactTriggerHandler.ContactBeforeInsert(trigger.new, trigger.newMap);
}
if(trigger.isUpdate){
ContactTriggerHandler.ContactBeforeUpdate(trigger.new, trigger.old, trigger.newMap, trigger.oldMap);
}
}
if(trigger.isAfter){
if(trigger.isInsert){
ContactTriggerHandler.ContactAfterInsert(trigger.new, trigger.newMap);
}
if(trigger.isUpdate){
ContactTriggerHandler.ContactAfterUpdate(trigger.new, trigger.old, trigger.newMap, trigger.oldMap);
}
}
}
Handler
public class ContactTriggerHandler {
public static void ContactBeforeInsert(list<Contact> newContacts, map<Id,Contact> newMap) {
myMethod1(newContacts, newMap);
myMethod2(newContacts, newMap);
}
public static void ContactBeforeUpdate(list<Contact> newContacts, list<Account> oldContacts, map<Id,Contact> newMap, map<Id,Contact> oldMap) {
myMethod3(newContacts, oldContacts, newMap, oldMap);
}
public static void ContactAfterInsert(list<Contact> newContacts, map<Id,Contact> newMap) {
myMethod2(newContacts, newMap);
myMethod4(newContacts, newMap);
}
public static void ContactAfterUpdate(list<Contact> newContacts, list<Account> oldContacts, map<Id,Contact> newMap, map<Id,Contact> oldMap) {
myMethod5(newContacts, oldContacts, newMap, oldMap);
}
public static void myMethod1(list<Contact> newContacts, map<Id,Contact> newMap){
//YOUR LOGIC
}
public static void myMethod2(list<Contact> newContacts, map<Id,Contact> newMap){
//YOUR LOGIC
}
public static void myMethod3(list<Contact> newContacts, list<Account> oldContacts, map<Id,Contact> newMap, map<Id,Contact> oldMap){
//YOUR LOGIC
}
public static void myMethod4(list<Contact> newContacts, map<Id,Contact> newMap){
//YOUR LOGIC
}
public static void myMethod5(list<Contact> newContacts, list<Account> oldContacts, map<Id,Contact> newMap, map<Id,Contact> oldMap){
//YOUR LOGIC
}
}
You have solved both of the problems mentioned above here. You can now reuse your code. You can call these public static methods from other places in order to reuse code. You can now also segment your testing and test individual smaller methods when testing your trigger, as you no longer have to make a DML call and run the whole trigger, you can just test individual methods.
Hopefully this handles some of your bulkification/best practices questions. There is actually quite a bit further you can go with optimizing but then we get into trigger frameworks and interfaces, but I think this is a decent start to some of the best practices when writing your triggers.
P.S. On a side note, this might be the kick I needed to actually start a blog as this turned out to be much lengthier than I originally planned.
First, here's a link to Developer Force's Current List of Best Practices for Logic which has links to several articles that apply to bulkifying your code.
Here's a link to the Trigger Pattern for Tidy, Streamlined, Bulkified Triggers which is a good place to start if you'd like to implement your triggers as classes and have any and all code for for a single object in one trigger; something which I believe is very important to do whenever possible.
With all that said, here are the general guidelines for what I focus my efforts on when writing a more complex trigger:
Initially I usually collect data into Sets
and Maps
from Trigger.new
, Trigger.old
or Trigger.newmap
, etc by iterating through FOR
loops. I normally use Sets instead of Lists to avoid collecting duplicates. There may be times when I actually want to collect all instances of something. When that's the case, I'll use Lists. Once I have my record Id's of interest, I can later retrieve any related data that's of interest from Trigger.oldmap or Trigger.newmap, so don't necessarily need to capture all of it up front unless its needed to help me determine which record ID's are of interest to me.
I DO NOT PUT a QUERY inside of a FOR
loop!
By having made these collections, If I need to query for information that's not stored in trigger.new
or trigger.old
, I can now run a single query outside of a FOR
loop. I minimize the number of queries I make in a trigger by determining the relationships between parents, children, grandchildren and other related objects, then making a single query wherever possible.
Sometimes that makes it necessary to return my query results as a Map
instead of a List
. When that happens, I use a FOR
loop to retrieve Lists of the different objects within the Map to put them into a form I can use. Example code below.
Map<Id, Account> AcctOppCustomObjectMap = new Map<Id, Account>([SELECT Id,(SELECT Id, xxx FROM xxx__r), (SELECT Id, yyy FROM yyy__r) FROM Account WHERE Id IN :idSet]);
List<yyy__c> yyyThings = new List<yyy__c>();
List<xxx__c> xxxxThings = new List<xxx__c>();
for (Id accountId : AcctOppCustomObjectMap.keyset()) {
Account acct = AcctOppCustomObjectMap.get(accountId);
// the acct reference will have child or related lists for your subqueries
// they will not be null, but they could be empty lists
List<xxx__c> xxxList = acct.xxx__r;
List<yyy__c> yyyList = acct.yyy__r;
// iteration of the child or related records is possible now using these two lists
for (xxx__c xThing : xxxList) {
// if there's filtering I need to do or other work on the list,
// I can do it in a loop like below
if(xThing.isEmpty() == false){
if(xThing.xxx == yThing.yyy) xThing.zzz = acct.Id
xxxThings.add(xThing);
}
}
for (yyy__c yThing : yyyList) {
// the same thing can be done for the other objects or both
if(yThing.yyy == xThing.xxx) yyyThings.add(yThing);
}
}
// I now have list xxxThings and list yyyThings pulled from the Map and sorted as needed
I perform all of my operations for creating, updating or deleting records before performing my DML on each object as a single operation. If there's more than one Object that requires DML, I've found its often best to call a separate class to do that with.
I try to avoid doing an update on the same object my trigger was called from. Doing that will cause my trigger to fire again. In that situation which is sometimes necessary, such as to reset a checkbox that called the trigger to begin with, the first thing I do in the trigger is test to see if that field's condition will allow me to exit the trigger (assuming that's the only criteria).
Additionally, I also avoid writing a trigger that requires a query be performed before it can determine if it needs to do anything (yes, sadly those kinds of triggers do exist).