Monday, 17 August 2015

Best Practice: Avoid SOQL Queries or DML statements inside FOR Loops

The previous Best Practice talked about the importance of handling all incoming records in a bulk manner. That example showed use of a for loop to iterate over all of the records in the Trigger.new collection. A common mistake is that queries or DML statements are placed inside a for loop. There is a governor limit that enforces a maximum number of SOQL queries. There is another that enforces a maximum number of DML statements (insert, update, delete, undelete). When these operations are placed inside a for loop, database operations are invoked once per iteration of the loop making it very easy to reach these governor limits.
Instead, move any database operations outside of for loops. If you need to query, query once, retrieve all the necessary data in a single query, then iterate over the results. If you need to modify the data, batch up data into a list and invoke your DML once on that list of data.
Here is an example showing both a query and a DML statement inside a for loop:

trigger accountTestTrggr on Account (before insert, before update) {
   
   //For loop to iterate through all the incoming Account records
   for(Account a: Trigger.new) {
         
      //THIS FOLLOWING QUERY IS INEFFICIENT AND DOESN'T SCALE
      //Since the SOQL Query for related Contacts is within the FOR loop, if this trigger is initiated 
      //with more than 100 records, the trigger will exceed the trigger governor limit
      //of maximum 100 SOQL Queries.
         
      List<Contact> contacts = [select id, salutation, firstname, lastname, email 
                        from Contact where accountId = :a.Id];
     
      for(Contact c: contacts) {
         System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], 
                                         LastName[' + c.lastname +']');
         c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname;
        
         //THIS FOLLOWING DML STATEMENT IS INEFFICIENT AND DOESN'T SCALE
         //Since the UPDATE dml operation is within the FOR loop, if this trigger is initiated 
         //with more than 150 records, the trigger will exceed the trigger governor limit 
         //of 150 DML Operations maximum.
                                
         update c;
      }       
   }
}

Pagination and the StandardSetController – No Custom Controller Required

<apex:page standardController="Opportunity" recordSetVar="opportunities" tabStyle="Opportunity">
    <apex:form >
        <apex:pageBlock title="Edit Opportunities" >
            <apex:pageMessages ></apex:pageMessages>
            <apex:pageBlockButtons >
              <apex:commandButton value="Save" action="{!save}"/>
              <apex:commandButton value="Cancel" action="{!cancel}"/>
            </apex:pageBlockButtons>
            <apex:pageBlockTable value="{!opportunities}" var="opp">
                <apex:column value="{!opp.name}"/>
                  <apex:column headerValue="Stage">
                    <apex:inputField value="{!opp.stageName}"/>
                  </apex:column>
                  <apex:column headerValue="Close Date">
                    <apex:inputField value="{!opp.closeDate}"/>
                  </apex:column>
            </apex:pageBlockTable>
            <apex:panelGrid cellpadding="5" cellspacing="5" columns="5" >
                <apex:commandButton value="|<" action="{!first}"  />
                <apex:commandButton value="<" action="{!previous}" rendered="{!HasPrevious}" />
                <apex:commandButton value=">" action="{!next}" rendered="{!HasNext}" />
                <apex:commandButton value=">|" action="{!last}"  />
                <apex:panelGroup >
                    <apex:outputText value="Records/Page"></apex:outputText>&nbsp;&nbsp;
                    <apex:selectList value="{!PageSize}" size="1">
                        <apex:selectOption itemValue="10" itemLabel="10"></apex:selectOption>
                        <apex:selectOption itemValue="15" itemLabel="15"></apex:selectOption>
                        <apex:selectOption itemValue="20" itemLabel="20"></apex:selectOption>
                    </apex:selectList>&nbsp;&nbsp;
                    <apex:commandButton action="{!NULL}" value="Update Page Size"/>
                </apex:panelGroup>
           </apex:panelGrid>
         </apex:pageBlock>
    </apex:form>
</apex:page>

Client-side sorting and pagination of an apex:pageBlockTable

public with sharing class DemoController {
    public Contact[] getContacts() {
        return [
                select FirstName, LastName, Birthdate, Email, LastModifiedDate, OwnerId
                from Contact
                order by Name
                limit 500
                ];
    }
}


<apex:page controller="DemoController">
 
<apex:stylesheet value="{!URLFOR($Resource.jQueryDataTablesZip, 'css/jquery.dataTables.css')}"/>
<style type="text/css">
.sorting {
    background: #f2f3f3 url('{! URLFOR($Resource.jQueryDataTablesZip, 'images/sort_both.png') }') no-repeat center right !important;
    padding-right: 20px !important;
}
.sorting_asc {
    background: #f2f3f3 url('{! URLFOR($Resource.jQueryDataTablesZip, 'images/sort_asc.png') }') no-repeat center right !important;
    padding-right: 20px !important;
}
.sorting_desc {
    background: #f2f3f3 url('{! URLFOR($Resource.jQueryDataTablesZip, 'images/sort_desc.png') }') no-repeat center right !important;
    padding-right: 20px !important;
}
.sorting_asc_disabled {
    background: #f2f3f3 url('{! URLFOR($Resource.jQueryDataTablesZip, 'images/sort_asc_disabled.png') }') no-repeat center right !important;
    padding-right: 20px !important;
}
.sorting_desc_disabled {
    background: #f2f3f3 url('{! URLFOR($Resource.jQueryDataTablesZip, 'images/sort_desc_disabled.png') }') no-repeat center right !important;
    padding-right: 20px !important;
}
table.dataTable tr.odd { background-color: white; }
table.dataTable tr.even { background-color: white; }
table.dataTable tr.odd td.sorting_1 { background-color: white; }
table.dataTable tr.odd td.sorting_2 { background-color: white; }
table.dataTable tr.odd td.sorting_3 { background-color: white; }
table.dataTable tr.even td.sorting_1 { background-color: white; }
table.dataTable tr.even td.sorting_2 { background-color: white; }
table.dataTable tr.even td.sorting_3 { background-color: white; }
.dataTables_length, .dataTables_filter, .dataTables_info, .dataTables_paginate {
    padding: 3px;
}
</style>
 
<apex:sectionHeader title="Data Tables Demo"/>
<apex:pageBlock >
    <apex:pageBlockSection columns="1">
        <apex:pageBlockTable value="{!contacts}" var="c" styleClass="dataTable">
            <apex:column value="{!c.FirstName}"/>
            <apex:column value="{!c.LastName}"/>
            <apex:column value="{!c.Birthdate}"/>
            <apex:column value="{!c.Email}"/>
            <apex:column value="{!c.LastModifiedDate}"/>
            <apex:column value="{!c.OwnerId}"/>
        </apex:pageBlockTable>
    </apex:pageBlockSection>
</apex:pageBlock>
 
<script type="text/javascript" language="javascript" src="{!URLFOR($Resource.jQueryDataTablesZip, 'js/jquery.js')}"></script>
<script type="text/javascript" language="javascript" src="{!URLFOR($Resource.jQueryDataTablesZip, 'js/jquery.dataTables.js')}"></script>
<script type="text/javascript" language="javascript">
var j$ = jQuery.noConflict();
j$('table.dataTable').dataTable({
    sPaginationType: "full_numbers"
});
</script>
 
</apex:page>