General trigger bulkification – best practices

A general question asked from time to time on stack exchange is question of trigger bulkification.

Therefore I have 2 questions:

  1. Which best practices are you using?

  2. What is the best way to handle updates of more than 200 records? (In case of more than 200 records the trigger is executed more than once.)

Answer

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.

  1. All this logic is buried in a trigger and is not reusable.
  2. 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.

Attribution
Source : Link , Question Author : Christian Deckert , Answer Author : Ratan Paul

Leave a Comment