Use
@future Appropriately
As articulated throughout this article, it is critical to write your Apex code
to efficiently handle bulk or many records at a time. This is also true for
asynchronous Apex methods (those annotated with the @future keyword).
Even though Apex written within an asynchronous method gets its own independent
set of higher governor limits, it still has governor limits. Additionally, no
more than ten @future methods can be invoked within a single Apex transaction.
· Here is a list of governor limits specific to the
@future annotation:
· No more than 10 method calls per Apex invocation
· No more than 200 method calls per Salesforce
license per 24 hours.
· The parameters specified must be primitive dataypes,
arrays of primitive datatypes, or collections of primitive datatypes.
· Methods with the future annotation cannot take
sObjects or objects as arguments.
· Methods with the future annotation cannot be used
in Visualforce controllers in either getMethodName or setMethodName methods,
nor in the constructor.
the
Apex trigger inefficiently invokes an asynchronous method for each Account
record it wants to process:
EX :
trigger
accountAsyncTrigger on Account (after insert, after update) {
for(Account a: Trigger.new){
// Invoke the @future method for each Account
// This is inefficient and will easily exceed the governor limit of
// at most 10 @future invocation per Apex transaction
asyncApex.processAccount(a.id);
}
}
Here is the
Apex class that defines the @future method:
EX :
global class
asyncApex {
@future
public
static void processAccount(Id accountId) {
List<Contact> contacts = [select id, salutation, firstname, lastname,
email
from Contact where accountId = :accountId];
for(Contact c: contacts){
System.debug('Contact Id[' + c.Id + '], FirstName[' +
c.firstname + '], LastName[' + c.lastname +']');
c.Description=c.salutation
+ ' ' + c.firstName + ' ' + c.lastname;
}
update contacts;
}
}
Since the
@future method is invoked within the for loop, it will be called N-times
(depending on the number of accounts being processed). So if there are more
than ten accounts, this code will throw an exception for exceeding a governor
limit of only ten @future invocations per Apex transaction.
Instead, the
@future method should be invoked with a batch of records so that it is only
invoked once for all records it needs to process:
EX:
trigger
accountAsyncTrigger on Account (after insert, after update) {
//By passing the @future method a set of Ids, it only needs to be
//invoked once to handle all of the data.
asyncApex.processAccount(Trigger.newMap.keySet());
}
And now the
@future method is designed to receive a set of records:
EX :
global class
asyncApex {
@future
public
static void processAccount(Set<Id> accountIds) {
List<Contact> contacts = [select id, salutation, firstname, lastname,
email from Contact where accountId IN :accountIds];
for(Contact c: contacts){
System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '],
LastName[' + c.lastname +']');
c.Description=c.salutation
+ ' ' + c.firstName + ' ' + c.lastname;
}
update contacts;
}
}
Writing Test
Methods to Verify Large Datasets :
Here is the poorly written contact trigger. For each contact, the trigger
performs a SOQL query to retrieve the related account. The invalid part of this
trigger is that the SOQL query is within the for loop and therefore will throw
a governor limit exception if more than 100 contacts are inserted/updated.
EX :
trigger
contactTest on Contact (before insert, before update) {
for(Contact ct: Trigger.new){
Account acct = [select id, name from Account where
Id=:ct.AccountId];
if(acct.BillingState=='CA'){
System.debug('found a
contact related to an account in california...');
ct.email =
'test_email@testing.com';
//Apply more logic
here....
}
}
}
Here is the
test method that tests if this trigger properly handles volume datasets:
EX :
public class
sampleTestMethodCls {
static testMethod void testAccountTrigger(){
//First, prepare 200 contacts for
the test data
Account acct = new
Account(name='test account');
insert acct;
Contact[] contactsToCreate = new
Contact[]{};
for(Integer x=0; x<200;x++){
Contact ct =
new Contact(AccountId=acct.Id,lastname='test');
contactsToCreate.add(ct);
}
//Now insert data causing an contact
trigger to fire.
Test.startTest();
insert contactsToCreate;
Test.stopTest();
}
}
Note the use
of Test.startTest and Test.stopTest. When executing tests, code called before
Test.startTest and after Test.stopTest receive a separate set of governor
limits than the code called between Test.startTest and Test.stopTest. This
allows for any data that needs to be setup to do so without affecting the
governor limits available to the actual code being tested.
Now let's
correct the trigger to properly handle bulk operations. The key to fixing this
trigger is to get the SOQL query outside the for loop and only do one SOQL
Query:
EX :
trigger
contactTest on Contact (before insert, before update)
{
Set<Id> accountIds = new Set<Id>();
for(Contact ct: Trigger.new)
accountIds.add(ct.AccountId);
//Do SOQL Query
Map<Id, Account> accounts = new Map<Id, Account>(
[select id, name, billingState from
Account where id in
:accountIds]);
for(Contact ct: Trigger.new){
if(accounts.get(ct.AccountId).BillingState=='CA'){
System.debug('found a contact related to an account in california...');
ct.email =
'test_email@testing.com';
//Apply more logic
here....
}
}
}
Note how the
SOQL query retrieving the accounts is now done once only. If you re-run the
test method shown above, it will now execute successfully with no errors and
100% code coverage.
Avoiding
Hardcoding :
Here is a sample that hardcodes the record type IDs that are used in an
conditional statement. This will work fine in the specific environment in which
the code was developed, but if this code were to be installed in a separate org
(ie. as part of an AppExchange package), there is no guarantee that the record
type identifiers will be the same.
EX :
for(Account
a: Trigger.new){
//Error - hardcoded the record type id
if(a.RecordTypeId=='012500000009WAr'){
//do some logic here.....
}else if(a.RecordTypeId=='0123000000095Km'){
//do some logic here for a different record
type...
}
}
}
Now, to properly handle the dynamic nature of the record type IDs, the
following example queries for the record types in the code, stores the dataset
in a map collection for easy retrieval, and ultimately avoids any hardcoding.
EX :
//Query for
the Account record types
List<RecordType> rtypes = [Select Name, Id From
RecordType
where sObjectType='Account' and isActive=true];
//Create a map between the Record Type Name and Id for
easy retrieval
Map<String,String> accountRecordTypes = new
Map<String,String>{};
for(RecordType rt: rtypes)
accountRecordTypes.put(rt.Name,rt.Id);
for(Account a: Trigger.new){
//Use the Map collection
to dynamically retrieve the Record Type Id
//Avoid hardcoding Ids
in the Apex code
if(a.RecordTypeId==accountRecordTypes.get('Healthcare')){
//do
some logic here.....
}else
if(a.RecordTypeId==accountRecordTypes.get('High Tech')){
//do
some logic here for a different record type...
}
}