In this blog not only we wll write the trigger logic but will also try to understand the logic building by doing sudo code, the complete trigger flow, test classes and best practices.
Let’s Get Started….
Apex Triggers
Enable you to perform custom actions before or after changes to Salesforce records, such as insertions, updates, or deletions. A trigger is Apex code that executes before or after the following types of operations: insert, update, delete, merge, upsert, undelete.
Trigger Best Practice
1) One Trigger Per Object
A single Apex Trigger is all you need for one particular object. If you develop multiple Triggers for a single object, you have no way of controlling the order of execution if those Triggers can run in the same contexts
2) Logic-less Triggers
If you write methods in your Triggers, those can’t be exposed for test purposes. You also can’t expose logic to be re-used anywhere else in your org.
3) Context-Specific Handler Methods
Create context-specific handler methods in Trigger handlers
4) Bulkify your Code
Bulkifying Apex code refers to the concept of making sure the code properly handles more than one record at a time.
5) Avoid SOQL Queries or DML statements inside FOR Loops
An individual Apex request gets a maximum of 100 SOQL queries before exceeding that governor limit. So if this trigger is invoked by a batch of more than 100 Account records, the governor limit will throw a runtime exception
6) Using Collections, Streamlining Queries, and Efficient For Loops
It is important to use Apex Collections to efficiently query data and store the data in memory. A combination of using collections and streamlining SOQL queries can substantially help writing efficient Apex code and avoid governor limits
7) Querying Large Data Sets
The total number of records that can be returned by SOQL queries in a request is 50,000. If returning a large set of queries causes you to exceed your heap limit, then a SOQL query for loop must be used instead. It can process multiple batches of records through the use of internal calls to query and queryMore
8) Use @future Appropriately
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). The differences between synchronous and asynchronous Apex can be found
9) Avoid Hardcoding IDs
When deploying Apex code between sandbox and production environments, or installing Force.com AppExchange packages, it is essential to avoid hardcoding IDs in the Apex code. By doing so, if the record IDs change between environments, the logic can dynamically identify the proper data to operate against and not fail
Enough of Theory
Now let’s get started with Sudo Code
Step 1
How contact and account are connected ?
We can connect account and related contacts using Accountid on contact
so let’s get contacts where Contact_Exist__c field value has changed and save the accountid in the set.
Set<id>accIdSet =new Set<id>(); Map<Id,Contact> contactAndLatestContactMap=new Map<Id,Contact>(); for(contact con : listnewcontacts){ if(con.lte !=null && mapofoldcontacts.get(con.id).Contact_Exist__c != con.Contact_Exist__c & con.accountid != null) conIdSet.add(con.accountid); }
Step 2
if there are multiple contacts related to account then we want to get contact based on last contact created date
Create a query to fetch contacts based on created date and check account id is in the set
if(!accIdSet.isEmpty()) { List<Contact> lstContact =[Select id, Contact_Exist__c from Contact where accountid In: accIdSet]; }
Step 3
Iterate over these contacts and put them in a map
for (contact con : lstContact){ //if map does not account id as key if(!contactAndLatestContactMap.containskey(con.accountid)) { //put accountid and contact in map. contactAndLatestContactMap.put(con.accountid,con); }
Step 4
Make Contact_Exist__c of account equals to contact’s Contact_Exist__c
for(Id accId:accIdSet){ account acc = new account(); acc.id = accid; acc.Contact_Exist__c= contactAndLatestContactMap.get(accid).Contact_Exist__c; }
FINAL CODE FOLLOWING BEST PRACTICES
GlobalConstants – Static Utility To store all the Static Global constant
Public Class GlobalConstants { //Constructor Public GlobalConstants(){ } Public Static final String CONTACT = 'Contact'; Public Static final String BYPASS_PERMISSION_API = 'Bypass_Logic'; Public Static final String SYSTEMADMIN = 'System Administrator'; }
Additional Points Question 1
Create a Custom Metadata Type that contains a checkbox called Trigger. This is set to true , and you do a check at the top of the trigger code whether it should run or not
Additional Points Solution 1
Custom Metadata:
Is customizable, deployable, packageable, and upgradeable application metadata. First, you create a custom metadata type, which defines the form of the application metadata. Then you build reusable functionality that determines the behavior based on metadata of that type.
When to use?
You want to easily control invoking different contexts of Apex Triggers, for example during data migration.
Solution
Create a Custom Metadata record for Apex Trigger and adjust the code. Then uncheck the checkbox to bypass specific Apex Trigger context.
Configure Custom Metadata
Create Custom Metadata object with checkbox fields for Apex Trigger and a text field
Get Custom Metadata record and check if a specific context is bypassed public static Boolean isTriggerSwitchEnabled(String sObjectAPIName){ List<Bypass_Switch__mdt> triggerSwitch = new List<Bypass_Switch__mdt>([Select Trigger__c From Bypass_Switch__mdt WHERE sObject_Name__c = :sObjectAPIName LIMIT 1]); return triggerSwitch[0].Trigger__c; }
Additional Points Question 2
Check if the Running User has a Custom Permission. Only when running user don’t have custom permission then execute logic?
Additional Point Solution 2
Custom permissions let you define access checks that can be assigned to users via permission sets or profiles, similar to how you assign user permissions and other access settings.
FeatureManagement’s static Boolean checkPermission(String customPermissionDeveloperName) will Efficiently Check if the Running User has a Custom Permission.
public static Boolean hasCustomPermission(String customPermissionAPIName){ return FeatureManagement.checkPermission(customPermissionAPIName); }
contactTrigger
trigger ContactTrigger on Contact (before insert,before update, after insert,after update, before delete, after delete) { if (ObjectUtils.isTriggerSwitchEnabled(GlobalConstants.CONTACT) && !ObjectUtils.hasCustomPermission(GlobalConstants.BYPASS_PERMISSION_API)){ if(trigger.isInsert) { ContactTriggerHandler.executeMethodsOnInsert(trigger.isAfter, trigger.isBefore, trigger.new, trigger.old,trigger.oldMap,trigger.newMap); } if(trigger.isUpdate) { ContactTriggerHandler.executeMethodsOnUpdate(trigger.isAfter, trigger.isBefore, trigger.new, trigger.old,trigger.oldMap,trigger.newMap); } } }
ContactTriggerHandler
public class ContactTriggerHandler { public static void executeMethodsOnInsert(Boolean isAfter, Boolean isBefore, List<Contact> newList, List<Contact> oldList,Map<Id,Contact> oldMap, Map<Id,Contact> newMap) { if(isAfter) { ContactTriggerHandlerService.updateConExsistingOnAccount(newList,oldMap,false); } } public static void executeMethodsOnUpdate(Boolean isAfter, Boolean isBefore, List<Contact> newList, List<Contact> oldList,Map<Id,Contact> oldMap, Map<Id,Contact> newMap) { if(isAfter) { ContactTriggerHandlerService.updateConExsistingOnAccount(newList,oldMap,true); } } }
ContactTriggerHandlerService
public static void updateConExsistingOnAccount(List<Contact> lstNewContact ,Map<Id,Contact> mapOldContacts,Boolean isAfterUpadate){ if(lstNewContact.size()>0){ List<Account> lstAccountToUpdate = new List<Account>(); Map<Id,Contact> AccountAndLatestContactMap=new Map<Id,Contact>(); Set<Id>accIdSet=new set<Id>(); if(isAfterUpadate){//on update for(Contact con : lstNewContact){ //check lte is not null and lte value has changed and contactlookup has value if( con.Contact_Exist__c != null && mapOldContacts.get(con.Id).Contact_Exist__c != con.Contact_Exist__c && con.accountId != null){ //add contact id related to account in set accIdSet.add(con.accountId); } } } //check accidset is not empty if(!accIdSet.isEmpty()) {//get all the contacts where accountid is in that set List<Contact> existingContactList=[select id,accountid,Contact_Exist__c from Contact where accountid In:accIdSet order by createdDate desc]; //loop over the contact for(Contact cont:existingContactList) {//if key is not having accountid then if(!contactAndLatestAccountMap.containskey(cont.accountid)) {//put id and contact in map contactAndLatestAccountMap.put(cont.accountid,cont); } } } for(Id accId:accIdSet){ //create instance of new account Account acc = new Account(); acc.Id = accId; //if map has accountid if(contactAndLatestAccountMap.containskey(accId)) //update account Contact_Exist__c equals contact's Contact_Exist__c acc.Contact_Exist__c = contactAndLatestAccountMap.get(accId).Contact_Exist__c; //update the list lstAccountToUpdate.add(con); } update lstAccountToUpdate; } }
TEST CLASSES
Returns a User to use method System.runAs(), usually to test Permission Sets in case of different Users (i.e. Standard User, System Admin, etc.)
public static User testUser(String userType, String contactID, String userRole, Boolean withInsert){ String profileQuery = String.escapeSingleQuotes('SELECT Id FROM Profile WHERE Name= :userType'); Profile p = Database.query(profileQuery); User sobjectData = new User(Alias='Test12', Email='test12@gmail.com', EmailEncodingKey='UTF-8', FirstName='testFN', LastName='testLN', LanguageLocaleKey='en_US', LocaleSidKey='en_US', ProfileId=p.Id, CommunityNickname='test12' +Integer.valueOf(math.rint(math.random()*10000))+System.currentTimeMillis(), TimeZoneSidKey='America/Los_Angeles', UserName='test'+Integer.valueOf(math.rint(math.random()*10000))+'@testorg'+System.currentTimeMillis()+'.com', IsActive=true, ContactId=contactID ); if (withInsert && sobjectData != null) { insert sobjectData; } return sobjectData; }
getGlobalDescribe() – Returns a map of all sObject names (keys) to sObject tokens (values) for the standard and custom objects defined in your organization.
describeSObjects(sObjectTypes)
This method is similar to the getDescribe method on the Schema.sObjectType token. Unlike the getDescribe method, this method allows you to specify the sObject type dynamically and describe more than one sObject at a time.
You can first call getGlobalDescribe to retrieve a list of all objects for your organization, then iterate through the list and use describeSObjects to obtain metadata about individual objects.
private static Map<String, Map<String,Id>> recordTypeIdMap = new Map<String, Map<String,String>>(); public static Map<String, String> RecordTypeIdMapFor(String objectName) { if(recordTypeIdMap.containsKey(objectName)){ return recordTypeIdMap.get(objectName); }else{ Map<String, Schema.SObjectType> describeMap = Schema.getGlobalDescribe(); if (!describeMap.containsKey(objectName)){ return null; } recordTypeIdMap.put(objectName,new Map<String,Id>()); List<Schema.DescribeSObjectResult> descResult = Schema.describeSObjects(new List<String>{objectName}); for (Schema.RecordTypeInfo each : descResult[0].getRecordTypeInfos()) { if (!each.isAvailable()){ continue; } recordTypeIdMap.get(objectName).put(each.getDeveloperName(), each.getRecordTypeId()); } return recordTypeIdMap.get(objectName); } }
Create an Account with RecordType(developer name). String argument is used to create the Name of the record. Boolean indicates whether the
record should be inserted before being returned.
public static Account testAccount(String accountName, String rectypeDeveloperName,Boolean withInsert) { Account sobjectData = new Account( Name = accountName, RecordTypeId = SObjectUtils.RecordTypeIdMapFor('Account').get(rectypeDeveloperName), BillingCity = 'Delhi', BillingCountry = 'India', BillingStreet = 'Noida Road '+ system.currentTimeMillis(), BillingPostalCode = '401002' ); if (withInsert && sobjectData != null){ insert sobjectData; } return sobjectData; }
GetRecordTypeIdMapTest
Asserts that the SObjectUtils.RecordTypeIdMapFor() method returns a map of record type ids when passed in the API Name of an SObject.
static testMethod void GetRecordTypeIdMapTest() { User testUser = TestDataFactory.testUser(GlobalConstants.SYSTEMADMIN, null, null, false); testUser.Email = 'test12@gmail.com'; insert testUser; System.runAs(testUser) { Map<String, String> recordTypeIdMap = SObjectUtils.RecordTypeIdMapFor('Account'); system.assert(recordTypeIdMap.size() > 1, 'there should be multiple record types for Account'); system.assert(recordTypeIdMap.containsKey('Test'), 'Account is expected to have an \'Test\' record type'); system.assertEquals(null, SObjectUtils.RecordTypeIdMapFor('wrong value'), 'method call should return a null value if the SObject name is not valid'); } }
ObjectUtilsTest
Asserts that boolean value returned correctly when the sobject passed into the method has valid values in the correct format.
static testMethod void isTriggerSwitchEnabledTest() { User testUser = TestDataFactory.testUser(GlobalConstants.SYSTEMADMIN, null, null, false); testUser.Email = 'test12@gmail.com'; insert testUser; System.runAs(testUser) { string sObjectName = 'Contact'; List<Bypass_Switch__mdt> triggerSwitch = new List<Bypass_Switch__mdt>([Select Trigger__c From Bypass_Switch__mdt WHERE sObject_Name__c = :sObjectName LIMIT 1]); system.assertEquals(triggerSwitch[0].Trigger__c, ObjectUtils.isTriggerSwitchEnabled('Contact'),'incorrect value returned from metadata'); } }
ContactTriggerHandlerTest
public class ContactTriggerHandlerTest { public static testMethod void testupdatConExsistingOnAccountObject(){ User u = TestDataFactory.testUser('System Administrator', null, null, true); List<Contact> listContact = [SELECT id, Contact_Exist__c,accountId from Contact LIMIT 1]; system.debug('listContact[0]== '+listContact[0]); List<Account> listAccount = [SELECT id,Contact_Exist__c from Account where id =: listContact[0].accountId]; Contact con = listContact[0]; Test.startTest(); Account objAccount = TestDataFactory.testAccount('Test Accountcontact',null, false); insert objAccount; System.runAs(u){ con.Contact_Exist__c='0062h00000QY2sS'; update con; } Test.stopTest(); system.assertEquals(con.Contact_Exist__c,listContact[0].Contact_Exist__c); } }