Triggers
- There should only be one trigger for each object.
Apex Trigger Codetrigger ContactTrigger on Contact (after delete, after insert, after undelete, after update, before delete, before insert, before update) { ContactTriggerHandler handler = new ContactTriggerHandler(Trigger.isExecuting, Trigger.size); if(Trigger.isInsert && Trigger.isBefore) { handler.OnBeforeInsert(Trigger.new); } else if(Trigger.isInsert && Trigger.isAfter){ handler.OnAfterInsert(Trigger.new); ContactTriggerHandler.OnAfterInsertAsync(Trigger.newMap.keySet()); } else if(Trigger.isUpdate && Trigger.isBefore){ handler.OnBeforeUpdate(Trigger.old, Trigger.new, Trigger.newMap); } else if(Trigger.isUpdate && Trigger.isAfter){ handler.OnAfterUpdate(Trigger.old, Trigger.new, Trigger.newMap); ContactTriggerHandler.OnAfterUpdateAsync(Trigger.newMap.keySet()); } else if(Trigger.isDelete && Trigger.isBefore){ handler.OnBeforeDelete(Trigger.old, Trigger.oldMap); } else if(Trigger.isDelete && Trigger.isAfter){ handler.OnAfterDelete(Trigger.old, Trigger.oldMap); ContactTriggerHandler.OnAfterDeleteAsync(Trigger.oldMap.keySet()); } else if(Trigger.isUnDelete){ handler.OnUndelete(Trigger.new); } }Apex Class Code
public with sharing class ContactTriggerHandler { private boolean isHandlerExecuting = false; private integer BatchSize = 0; public ContactTriggerHandler(boolean isExecuting, integer size){ isHandlerExecuting = isExecuting; BatchSize = size; } public void OnBeforeInsert(Contact[] newContacts){ //Example usage for(Contact newContact : newContacts){ if(newContact.Name == null){ newContact.Name.addError('Missing Name Field'); } } } public void OnAfterInsert(Contact[] newContacts){ } @future public static void OnAfterInsertAsync(Set newContactIDs){ //Example usage List newContacts = [select Id, Name from Contact where Id IN :newContactIDs]; } public void OnBeforeUpdate(Contact[] oldContact, Contact[] updatedContacts, Map contactMap){ //Example Map usage Map contacts = new Map( [select Id, FirstName, LastName, Email from Contact where Id IN :contactMap.keySet()] ); } public void OnAfterUpdate(Contact[] oldContacts, Contact[] updatedContacts, Map contactMap){ } @future public static void OnAfterUpdateAsync(Set updatedContactIDs){ List updatedContacts = [select Id, Name from Contact where Id IN :updatedContactIDs]; } public void OnBeforeDelete(Contact[] contactsToDelete, Map contactMap){ } public void OnAfterDelete(Contact[] deletedContacts, Map contactMap){ } @future public static void OnAfterDeleteAsync(Set deletedContactIDs){ } public void OnUndelete(Contact[] restoredContacts){ } public boolean IsTriggerContext{ get{ return isHandlerExecuting;} } public boolean IsVisualforcePageContext{ get{ return !IsTriggerContext;} } public boolean IsWebServiceContext{ get{ return !IsTriggerContext;} } public boolean IsExecuteAnonymousContext{ get{ return !IsTriggerContext;} } }
- Use Helper Classes
https://github.com/thysmichels/Force.com-Helper-Classes
https://github.com/thysmichels/base
http://code.google.com/p/apex-lang/
Dynamic Delete Batch Apex Classglobal class dynamicDelete implements Database.Batchable{ global final String Query; global dynamicDelete(String q) { Query=q; } global Database.QueryLocator start(Database.BatchableContext BC) { return Database.getQueryLocator(query); } global void execute(Database.BatchableContext BC, List scope) { delete scope; } }Scheduler for Batch Apex Delete
global class dynamicDeleteScheduler implements Schedulable{ private static final String deleteLeads = 'Select id from Lead where Email like \'%test%\' limit 5000'; private static final String deleteTasks = 'Select id from Task where WhoId IN (Select id from Contact where email like \'%test%\') limit 50000'; private static final String deleteOpportunities = 'Select id from Opportunity where id in (Select OpportunityId from OpportunityContactRole where Contact.Email like \'%test%\') limit 50000'; private static final String deleteCases = 'Select id from Case where ContactId IN (Select id from Contact where Email like \'%test%\') limit 50000'; private static final String deleteTestAccounts = 'Select id from Account where Email__c like \'%test%\' limit 50000'; private static final String deleteTestContacts = 'Select id from Contact where Email like \'%test%\' limit 50000'; global void execute(SchedulableContext SC) { dynamicDelete deleteTask = new dynamicDelete(deleteTasks); Database.executebatch(deleteTask); dynamicDelete deleteCase = new dynamicDelete(deleteCases); Database.executebatch(deleteCase); dynamicDelete deleteOpportunity = new dynamicDelete(deleteOpportunities); Database.executebatch(deleteOpportunity); dynamicDelete deletecontact = new dynamicDelete(deleteTestContacts); Database.executebatch(deletecontact); dynamicDelete deleteaccount = new dynamicDelete(deleteTestAccounts); Database.executebatch(deleteaccount); } }Bulkify Triggers to process up to 200 records for each call. This will help you avoid hitting DML limit per process.
Bulk your DML operations with with List/Maps/Setsprivate void bulkUpdateExample(Map updateContactOwner) { List<Contact> listToHoldListOfContactsToBeUpdated = new List<Contact>(); Id salesForceAdminId = [Select id from User where Name='Salesforce Administrator'].Id; for (Id contactIdToUpdateOwner : updateContactOwner.keySet()) { updateContactOwner.get(contactIdToUpdateOwner).OwnerId = salesForceAdminId; //Add all records to a List/Map/Set to be updated outside the for loop listToHoldListOfContactsToBeUpdated.add(updateContactOwner.get(contactIdToUpdateOwner)); } //Always update outside a for loop update listToHoldListOfContactsToBeUpdated; }Bulkify your Test classes with at least 200 records
@isTest(SeeAllData=false) static void testContactOwnerReassignmentSuccess() { Account createAccount = new Account(Name='[TEST]'); insert createAccount; List contactList = new List(); for (int k = 0; k < 200; k++) contactList.add(new Contact(FirstName='[Test]' + k, AccountId=createAccount.Id)); Test.startTest(); insert contactList; Test.stopTest(); System.assert(200 == [Select count() from Contact where Owner.Name = 'Salesforce Administrator']); }Use Collections in SOQL “WHERE” clauses to retrieve all records using a single query. Also use self, semi and anti joins:
legalDocumentNames = new String[] { 'Employment Application', 'Release of Liability', 'Non-Disclosure Agreement', '%Employment Agreement' } List legalDocumentQuery = [select ContactId from Asset where Product2.Name Like :legalDocumentNames];Apex
Use @future annotation for methods that can be executed asynchronously
@future public static void OnAfterInsertAsync(Set newAssetIDs) { new smbNurtureCampaign().insertCampaignMemebersToControlAndTestCampaign(newAssetIDs); }Provide proper exception handling.
Apply try catch to all methodspublic static User getSalesRepsForRouting(List getListActiveUser) { try { return getListActiveUser.size() == 1 ? getListActiveUser[0] : getListActiveUser[0]; } catch(handlerException e) { Debugger.createDebugRecord(e, 'ACRoundRobinV5'); sendErrorMail(e.getMessage()); return null; } }Extend the Exception Class
public class handlerException extends Exception{}Catch Exception and create record in custom Debugger Object
public static void createDebugRecord(handlerException exceptionObject, String methodName) { Database.insert(new Debugger__c ( Message__c = exceptionObject.getMessage(), Line_Number__c = exceptionObject.getLineNumber(), Stack_Trace__c = exceptionObject.getStackTraceString(), Method_Name__c=methodName ), false); } public static void createDebugRecord(DMLException dmlexceptionObject, String methodName) { Database.insert(new Debugger__c ( DML_Record_Id__c = dmlexceptionObject.getDmlId(0), DML_Type__c = dmlexceptionObject.getTypeName(), Message__c = dmlexceptionObject.getDmlMessage(0), Line_Number__c = dmlexceptionObject.getDmlIndex(0), Stack_Trace__c = dmlexceptionObject.getDmlStatusCode(0), Method_Name__c=methodName ),false); }Prevent SOQL and SOSL injection attacks by using static queries, binding variables or the escapeSingleQuotes method.
private static final String[] QUERY_SELECT_STATEMENT = new String[]{'select ',' from ', ' where Id = \''}; String query = QUERY_SELECT_STATEMENT[0]; for(Schema.DescribeFieldResult dfr : queryFields) query = query + dfr.getName() + ','; query = query.subString(0,query.length() - 1); query = query + QUERY_SELECT_STATEMENT[1]; query = query + obj.getName(); query = query + QUERY_SELECT_STATEMENT[2]; query = query + theId + '\'';When querying large data sets, use a SOQL “for” loop
for (Account accountToCheckAndUpdateAccountDetails : [Select BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry, ShippingStreet, ShippingCity, ShippingState, ShippingPostalCode, ShippingCountry, Phone, Fax, Website, Industry from Account]) { ..... } update accountToCheckAndUpdateAccountDetails;Use Apex Limits Methods to avoid hitting governor exceptions.
In the if statement we check if it is save to proceed without hitting governor limitsList accountOwnerDetail = [SELECT OwnerId, Owner.Name, Owner.Profile.Name, Owner.isActive from Account]; if (accountOwnerDetail.size() + Limits.getDMLRows() < Limits.getLimitDMLRows()) { ... }No SOQL or SOSL queries inside loops
Create parent-to-child and child-to-parent relationship queries outside for loopsContact getContactWithAccount = Database.query('Select Name, Email, Account.Name, Account.BillingStreet from Contact limit 1'); Account getAccountContactOpportunityInOneQuery = Database.query('Select Id, (Select Name from Account.Contacts), (Select Name from Account.Opportunities) from Account limit 1');Tip: Update an Object without querying for it
List accountFieldsToBeUpdated = new List(); for (Contact updateAccountsFieldFromContact : [Select AccountId from Contact where OtherCity ='San Francisco']) { //create new account object with the accountId from contact Account newAccountObject = new Account(Id=updateAccountsFieldFromContact.AccountId); newAccountObject.ShippingCity = 'SF'; newAccountObject.BillingCity = 'SF'; accountFieldsToBeUpdated.add(newAccountObject); } update accountFieldsToBeUpdated ;Do not use hardcoded IDs: As ID's are unique and different between environments.
Don't use ID's rather get Id's by NameOpportunity getOpportunity = [Select Name from Opportunity where Owner.Id='00560000001S1eh' limit 1];Rather do the following as your unit tests will fail in other environments
Opportunity getOpportunity = [Select Name from Opportunity where Owner.Name='John Black' limit 1];Visualforce
Do not hardcode picklists in Visualforce pages; include them in the controller instead.
Picklist Controller Methodpublic List getFilterByRecordTypeList() { List<SelectOption> op = new List<SelectOption>(); Set<Id> setPPFamily = new Set<Id>(); setPPFamily.clear(); setPPFamily.add('All Opportunities'); List<AggregateResult> aggregateResultsOfMotivatingProductFamily = [SELECT RecordType.Name FROM Opportunity Group By RecordType.Name]; for (AggregateResult ppfamilyname : aggregateResultsOfMotivatingProductFamily) op.add(new SelectOption((String)ppfamilyname.get('RecordType.Name'),(String)ppfamilyname.get('RecordType.Name'))); op.sort(); return op; }Calling Picklist Controller in Visualforce
<apex:outputText style="color:#f8f8ff;font-weight:bold" value="Filter By Document Type"/> <apex:toolbarGroup itemSeparator="line" location="left" id="toobarGroupForm2"> <apex:selectList label="Filter by Record Type" value="{!FilterByRecordType}" size="1" required="false" > <apex:actionSupport event="onchange" action="{!FilterByRecordTypeAction}" status="filterByRecordType" reRender="innerblock"/> <apex:outputPanel style="color:#f8f8ff;font-weight:bold"> <apex:actionStatus id="filterByRecordType" startText="Filtering your records..." stopText=""/> </apex:outputPanel> <apex:selectOptions value="{!FilterByRecordTypeList}"/> </apex:selectList> </apex:toolbarGroup>Javascript and CSS should be included as Static Resources allowing the browser to cache them.
Import your JS and CSS to the Static Resources as Zip of single files. Include CSS on top and JS at the bottom for faster loading<apex:page controller="YourCustomController" sidebar="true" cache="true" showChat="true" > <head> <apex:stylesheet value="{!URLFOR($Resource.jQuery1, '/css/ui-lightness/jquery-ui-1.8.23.custom.css')}" /> </head> <body> ....
<apex:includeScript value="{!URLFOR($Resource.jQuery1, '/js/jquery-1.8.0.min.js')}" />
<apex:includeScript value="{!URLFOR($Resource.jQuery1, '/js/jquery-ui-1.8.23.custom.min.js')}" />
</body>
</apex:page>
Mark controller variables as “transient” if they are not needed between server calls. This will make your page load faster as it reduces the size of the View State.
Transient Example Visualforce Page<apex:page controller="ExampleController"> <apex:form id="theForm"> <apex:panelBar id="thePanel"> <apex:panelbarItem label="Transient vs Non Transient"> <b>Transient Time: </b> {!t1} <br /> <b>Non Transient Time: </b> {!t2} </apex:panelbarItem> <apex:actionPoller interval="5" enabled="true" rerender="thePanel" action="{!refresh}" /> </apex:panelBar> </apex:form> </apex:page>Transient Example Controller
public class ExampleController { public PageReference refresh() { return null; } DateTime t1; transient DateTime t2; public String getT1() { if (t1 == null) t1 = System.now(); return '' + t1; } public String getT2() { if (t2 == null) t2 = System.now(); return '' + t2; } }Use the <repeat> tag to iterate over large collections.
<apex:page standardController="Opportunity" id="thePage"> <table id="EquipmentTable" border="1"> <apex:repeat value="{!Opportunity}" var="line" id="theRepeat"> <tr> <td valign="top"><apex:outputField value="{!line.Id}" /></td> <td valign="top"><apex:outputField value="{!line.Name}" /></td> <td valign="top"><apex:outputField value="{!line.Probability}" /></td> <td valign="top"><apex:outputField value="{!line.Amount}" /></td> </tr> </apex:repeat> </table> </apex:page>