A Bulkifying Pattern with Apex and SOQL

One of the cardinal rules for programming with Apex is never make a SOQL query in a loop.  Even when you’re 110% sure that the loop will make no more than 2 passes, you migrate your code from the sandbox to production and… BOOM!  SOQL query limit exceeded!  This is particularly bad in code that is not customer facing because it may be some time before the failure is apparent.

The answer is to always bulkify your queries.  As an example, you want to get all the Accounts assigned to the current User, then you want all the Contacts for each Account.  First, the non-bulkified example:

// We want a Map of Account IDs to all associated Contact IDs
Map <ID, List <ID>> accountContactMap = new Map <ID, List <ID>>();

// Select all Accounts owned by the current User
List <Account> accountList =
    [Select ID, Name From Account Where OwnerId =:UserInfo.getUserId()];

// For each Account get the associated Contacts
for (Account nextAccount : accountList) {
    // Bad!  Very bad!  No SOQL in a loop!  What was I thinking!?!??
    // Get the Contacts associated with this Account
    List <Contact> nextContacts =
        [Select ID, Name From Contact Where AccountId =:nextAccount.ID];
    System.debug('Account '+nextAccount.Name+' has '+nextContacts.size()+' Contacts');
   // Create a List of all the associated Contacts
   List <ID> contactIDs = new List <ID> ();
   for (Contact nextContact : nextContacts) {
        contactIDs.add(nextContact.ID);
    }
    // Add them to the Map
    accountContactMap.put(nextAccount.ID, contactIDs);
}

Now the bulkified version:

 1  // We want a Map of Account IDs to all associated Contact IDs
 2  Map <ID, List <ID>> accountContactMap = new Map <ID, List <ID>>();
 3  
 4  // Select all Accounts owned by the current User
 5  List <Account> accountList =
 6      [Select ID, Name From Account Where OwnerId =:UserInfo.getUserId()];        
 7  
 8  // Make a List of Account IDs
 9  List <ID> accountIDList = new List <ID> ();
10  for (Account nextAccount : accountList) {
11      accountIDList.add(nextAccount.ID);
12  }        
13  
14  // Now get the Contacts associated with the Accounts
15  List <Contact> allContactList =
16      [Select ID, Name        
17          From Contact        
18          Where AccountId in :accountIDList        
19          order by AccountId];        
20  if (allContactList.size() == 0) {        
20      return;        
21  }
22  
23  List <Contact> contactList = new List <Contact> ();
24  ID previousAcctID = contactList[0].AccountID;
25  
26  for (Contact nextContact : contactList) {
27      if (nextContact.AccountID != previousAcctID) {        
28          accountContactMap.put(previousAcctID, contactList);
29          contactList = new List <Contact> ();
30          previousAcctID = nextContact.AccountID;        
31      }
32      contactList.add(nextContact.ID);        
33  }
34  accountContactMap.put(previousAcctID, contactList);

The new code is longer and not as readable, but we’ve gotten rid of the dangerous SOQL in the for loop.

Notice the order by AccountId on line 19.  This is critical to insure that we make a clean transition from Contacts of one Account to another.  If you are using this pattern for more dimensions than just ID, you may have to order by multiple fields.  For example, let’s say you have a custom field Last_Ping_Date__c on Contact.  You need the Map of Account IDs to Contact IDs and you will be charting them by date.  This is simple to do in the query as shown here:

    // Now get the Contacts associated with the Accounts
    // Order by date so the chart is properly ordered
    List <Contact> allContactList =
        [Select ID, Name        
         From Contact        
         Where AccountId in :accountIDList        
         order by AccountId, Last_Ping_Date__c]; 
      

Don’t forget to test for nulls and empty collections.  The return statement on line 20 is required because we are referencing field 0 in the List on line 24.

Finally, notice the second accountContactMap.put() on line 34.  We can’t forget to add the final Account ID and its Contacts to the Map.

One consequence of this pattern is a generous use of script statements.  Of course, that may run into a different governor limit of a maximum 200,000 script statements.  Apex programming with large data sets can be a juggling act – staying under some limits may bump into others.  However, with only 100 SOQL statements to work with, bulkifying your queries should always be your first step.

Photo Credit: mccoryjames on flikr