Learning Apex Triggers (Part 2) – Trigger Handler Pattern Implementation

Learning Apex Triggers (Part 2) – Trigger Handler Pattern Implementation

, ,

Introduction:

Welcome to the continuation of our previous post Learning Apex Triggers (Part1). Let’s explore the trigger handler pattern implementation. We’ll also discuss some best practices for triggers in the code in this post.

Implementing a Trigger Handler Pattern in Salesforce is a widely recognized best practice. It enhances the organization and maintainability of your Apex triggers. By embracing the Trigger Handler Pattern, you align with industry standards, fostering a robust and scalable Salesforce development environment.Here’s an overview of its features, along with the pros and cons:

Features:

  • Separation of Concerns: Isolating trigger logic into handler classes achieves a clear distinction. This separation is between database operations and business logic. This promotes cleaner code architecture.
  • Context-Specific Methods: Handler classes can define methods tailored to specific trigger contexts (e.g., beforeInsert, afterUpdate), allowing precise control over the execution flow.
  • Reusability: Encapsulating logic within handler classes enables reuse across different triggers and scenarios, reducing code duplication.

Pros:

  • Improved Maintainability: With logic centralized in handler classes, updates and debugging become more straightforward, enhancing code maintainability.
  • Enhanced Readability: Triggers remain concise, delegating complex operations to handlers, which improves overall code readability.
  • Facilitates Testing: Isolated logic in handler classes allows for more focused unit testing. This leads to efficient testing and robust, reliable code.
  • Scalability: This pattern supports the addition of new functionalities with minimal disruption to existing code, aiding in scalable application development.

Cons:

  • Initial Complexity: Setting up the Trigger Handler Pattern requires an upfront investment. This involves designing the architecture. This approach may introduce initial complexity.
  • Potential Overhead: For simple triggers, implementing a handler pattern might seem like over-engineering, potentially adding unnecessary overhead.

Here’s how this pattern operates:

1. Trigger Definition:

  • A single trigger is defined for the object (e.g., Contact) to handle various events such as before insert, after update, etc.
  • This trigger acts as an entry point, delegating the actual processing to a handler class.

2. Handler Class:

  • A dedicated Apex class (e.g., UpdateContactCountOnAccountHandler) contains methods corresponding to each trigger event.
  • These methods encapsulate the business logic for each event, ensuring that the trigger remains free of complex logic.

3. Data Encapsulation:

  • The handler class often includes an inner class (e.g., ContactData) that encapsulates trigger context variables like Trigger.new, Trigger.old, and their maps.
  • This encapsulation simplifies data access and enhances code readability.

4. Event Handling:

  • The handler class typically includes a method (e.g., handleEvent) that uses a switch statement to determine the type of trigger event and calls the corresponding method.
  • Each event-specific method (e.g., handleAfterInsert, handleBeforeUpdate) contains the logic specific to that event.

5. Bulk Processing:

  • The handler methods are designed to handle bulk operations efficiently, ensuring that the code adheres to Salesforce governor limits.

6. Separation of Concerns:

  • The trigger delegates logic to the handler class. This keeps the trigger focused solely on delegating control. It adheres to the principle of separation of concerns.

7. Maintainability and Scalability:

  • This pattern allows for easier maintenance and scalability. You can add or modify logic for a specific event within the handler class. This is done without altering the trigger itself.
Trigger Explanation:

Lets. take an example of our sample code – Updating total contacts count on Account using apex trigger and aggregate query (updating child record count on parent record in a look up relationship), and try to implement the handler pattern as below –

  • We updated the trigger to remove the logic and handle the logic in the handler class.
  • The below code defines an Apex trigger in Salesforce. This trigger executes before and after performing certain operations. These operations include insert, update, delete, and undelete on Contact records.
  • The trigger creates a new map called “contactsData” that stores the current and previous versions of the Contact records affected by the operation (stored in the Trigger.new and Trigger.old variables, respectively) during operation (insert, update, delete, and undelete).
  • The map also stores maps of the current and previous versions of the Contact records (stored in the Trigger.newMap and Trigger.oldMap variables, respectively).
  • The trigger invokes a method named “handleEvent”. This method is on an implementation class named “UpdateContactCountOnAccountHandler”. The invocation occurs following the creation of a new map known as “contactsData”.
  • The trigger initializes a new map called “contactsData.” This map stores the current and previous versions of the Contact records affected by the operation. It stores the records in the Trigger.new and Trigger.old variables, respectively.
  • It then calls a method called “handleEvent” on an implementation class called “UpdateContactCountOnAccountHandler” in the trigger handler pattern. The trigger passes in the operation type, stored in the Trigger.operationType variable, and an instance of a custom class called “ContactData,” initialized with the “contactsData” map.
Sample Trigger Code :
// Apex trigger to update contact count on related account
// before insert, before update, before delete, after insert, after update, after delete, after undelete
trigger UpdateContactCountOnAccount on Contact (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
    // Create a map to store current and previous versions of the Contact records
    Map<String,Object> contactsData = new Map<String,Object>();
    // Add current version of the Contact records to the map
    contactsData.put('triggerNew',Trigger.new);
    // Add map of current versions of the Contact records to the map
    contactsData.put('triggerNewMap',Trigger.newMap);
    // Add previous version of the Contact records to the map
    contactsData.put('triggerOld',Trigger.old);
    // Add map of previous versions of the Contact records to the map
    contactsData.put('triggerOldMap',Trigger.oldMap);
    // Call handleEvent method on UpdateContactCountOnAccountHandler class and pass in the operation type and ContactData object
    UpdateContactCountOnAccountHandler.handleEvent(Trigger.operationType, new UpdateContactCountOnAccountHandler.ContactData(contactsData));
}

Trigger Handler Explanation:
  • The class declares a public static variable called “accountIds”. It’s a set of Ids used to store the Ids of the parent Account objects that require updating.
  • The trigger enters through the “handleEvent” method, which accepts two parameters: “operationType” and “conData”. The “operationType” parameter defines the type of trigger operation that happened (e.g. before insert, after update, etc.). The “conData” parameter contains the impacted data.
  • The “handleEvent” method uses a switch statement to determine the operation type. It calls the appropriate method to handle that type of operation.
  • The class includes several methods for handling the different types of trigger operations.
  • For instance, when a Contact record is inserted, the “handleAfterInsert” method executes. It iterates through the newly inserted Contact records to add their parent Account Ids to the “accountIds” set.
  • The “handleAfterUpdate” method is called when a Contact record is updated. It checks if the parent Account Id of the Contact has been changed. If so, it adds both the old and new parent Account Ids to the “accountIds” set.
  • The “handleAfterDelete” method is called when a Contact record is deleted. It adds the parent Account Id of the deleted Contact record to the “accountIds” set.
  • At the end of each handle method, it calls the updateAccounts method. The updateAccounts method updates the Number of Contacts field on the account object. It retrieves the number of contacts count for each account and updates the account.
Sample Trigger Handler Code :
// TriggerHandler Class
public without sharing class UpdateContactCountOnAccountHandler {
    // Declare a set to store the Ids of the parent object Account
    public static Set<Id> accountIds = new Set<Id>();
    /**
     * Method to handle the trigger event
     * @param operationType The operation type that the trigger is handling
     * @param conData The data for the Contact trigger
     */
    public static void handleEvent(System.TriggerOperation operationType, ContactData conData) {
        // switch statement to handle different operation types
        switch on operationType {
            // handle before insert event
            when BEFORE_INSERT {
                UpdateContactCountOnAccountHandler.handleBeforeInsert(conData.triggerNew);
            }
            // handle after insert event
            when AFTER_INSERT {
                UpdateContactCountOnAccountHandler.handleAfterInsert(
                    conData.triggerNew,
                    conData.triggerNewMap
                );
            }
            // handle before update event
            when BEFORE_UPDATE {
                UpdateContactCountOnAccountHandler.handleBeforeUpdate(
                    conData.triggerNew,
                    conData.triggerOldMap
                );
            }
            // handle after update event
            when AFTER_UPDATE {
                UpdateContactCountOnAccountHandler.handleAfterUpdate(
                    conData.triggerNew,
                    conData.triggerOldMap
                );
            }
            // handle before delete event
            when BEFORE_DELETE {
                UpdateContactCountOnAccountHandler.handleBeforeDelete(
                    conData.triggerOld,
                    conData.triggerOldMap
                );
            }
            // handle after delete event
            when AFTER_DELETE {
                UpdateContactCountOnAccountHandler.handleAfterDelete(
                    conData.triggerOld,
                    conData.triggerOldMap
                );
            }
            // handle after undelete event
            when AFTER_UNDELETE {
                UpdateContactCountOnAccountHandler.handleAfterUndelete(
                    conData.triggerNew,
                    conData.triggerNewMap
                );
            }
        }
    }
    /**
     * Method to handle before insert event
     * @param triggerNew The list of new Contact records that were inserted
     */
    public static void handleBeforeInsert(List<Contact> triggerNew) {
        // your code to handle before insert event
        System.debug('handleBeforeInsert+++');
    }
    /**
     * Method to handle after insert event
     * @param triggerNew The list of new Contact records that were inserted
     * @param triggerOldMap The map of old Contact records before being updated
     */
    public static void handleAfterInsert(List<Contact> triggerNew, Map<Id, Contact> triggerOldMap) {
        // your code to handle after insert event
        // loop through the new contacts
        for (Contact child : triggerNew) {
            // Check if the parent account Id is not null
            if (child.AccountId != null) {
                // Add the parent account Id of each new contact record to the set of parent account Ids
                accountIds.add(child.AccountId);
            }
        }
        if (!accountIds.isEmpty()) {
            updateAccounts(accountIds);
        }
    }
    /* Method to handle before update event
     * @param triggerNew The list of new Contact records that were inserted
     * @param triggerOldMap The map of old Contact records before being updated
     */
    public static void handleBeforeUpdate(
        List<Contact> triggerNew,
        Map<Id, Contact> triggerOldMap
    ) {
        // your code to handle before update event
        System.debug('handleBeforeUpdate+++');
    }
    /* Method to handle after update event
     * @param triggerNew The list of new Contact records that were inserted
     * @param triggerOldMap The map of old Contact records before being updated
     */
    public static void handleAfterUpdate(List<Contact> triggerNew, Map<Id, Contact> triggerOldMap) {
        // your code to handle after update event
        //loop through the updated contacts
        for (Contact child : triggerNew) {
            // Check if the parent Id is changed
            if (child.AccountId != triggerOldMap.get(child.Id).AccountId) {
                // Check if either the old parent Id or the value field of the child record has been changed
                // and if so, it adds the old parent Id to the set of parent Ids to be updated.
                if (triggerOldMap.get(child.Id).AccountId != null) {
                    accountIds.add(triggerOldMap.get(child.Id).AccountId);
                }
                // Check if the new parent account Id is not null
                if (child.AccountId != null) {
                    // Add the parent account Id of each updated contact record to the set of parent account Ids
                    accountIds.add(child.AccountId);
                }
            }
        }
        if (!accountIds.isEmpty()) {
            updateAccounts(accountIds);
        }
    }
    /* Method to handle before delete event
     * @param triggerNew The list of old Contact records that were inserted
     * @param triggerOldMap The map of old Contact records before being updated
     */
    public static void handleBeforeDelete(
        List<Contact> triggerOld,
        Map<Id, Contact> triggerOldMap
    ) {
        // your code to handle before delete event
        System.debug('handleBeforeDelete+++');
    }
    /* Method to handle after delete event
     * @param triggerNew The list of old Contact records that were inserted
     * @param triggerOldMap The map of old Contact records before being updated
     */
    public static void handleAfterDelete(List<Contact> triggerOld, Map<Id, Contact> triggerOldMap) {
        // your code to handle after delete event
        // loop through the deleted contacts
        for (Contact child : triggerOld) {
            // Add the parent account Id of each deleted child record to the set of parent Ids to be updated.
            accountIds.add(child.AccountId);
        }
        if (!accountIds.isEmpty()) {
            updateAccounts(accountIds);
        }
    }
    /* Method to handle after undelete event
     * @param triggerNew The list of new Contact records that were inserted
     * @param triggerOldMap The map of new Contact records before being updated
     */
    public static void handleAfterUndelete(
        List<Contact> triggerNew,
        Map<Id, Contact> triggerNewMap
    ) {
        // your code to handle after undelete event
        // loop through the undeleted contacts
        for (Contact child : triggerNew) {
            // Check if the parent account Id is not null
            if (child.AccountId != null) {
                accountIds.add(child.AccountId);
            }
        }
        if (!accountIds.isEmpty()) {
            updateAccounts(accountIds);
        }
    }
    /**
     * Method to update the account records with the number of contacts
     * @param accountIds set of account Ids to update
     */
    public static void updateAccounts(Set<Id> accountIds) {
        // Declare a map to store the number of contacts for each parent
        Map<Id, Integer> contactsCount = new Map<Id, Integer>();
        // Get the contact count for each account Id
        for (AggregateResult ar : [
            SELECT AccountId, COUNT(Id) contactCount
            FROM Contact
            WHERE AccountId IN :accountIds
            WITH SECURITY_ENFORCED
            GROUP BY AccountId
        ]) {
            // put the account Id and the contact count in the map
            contactsCount.put((Id) ar.get('AccountId'), (Integer) ar.get('contactCount'));
        }
        // Create a list of parent records to update
        List<Account> parentsToUpdate = new List<Account>();
        // Loop through the parent records to update the number of contacts field
        for (Account parent : [
            SELECT Id, Number_of_Contacts__c
            FROM Account
            WHERE Id IN :accountIds
            WITH SECURITY_ENFORCED
        ]) {
            // check if the account Id is present in the map
            if (contactsCount.containsKey(parent.Id)) {
                //update the number of contact field
                parent.Number_of_Contacts__c = contactsCount.get(parent.Id);
            } else {
                // if not present set the number of contact to 0
                parent.Number_of_Contacts__c = 0;
            }
            // add the parent account to the list to update
            parentsToUpdate.add(parent);
        }
        // Update the parent object
        if (!parentsToUpdate.isEmpty()) {
            if (Schema.sObjectType.Account.isUpdateable()) {
                update parentsToUpdate;
            }
        }
    }
    // ContactData class
    public class ContactData {
        // Declare a list variable to store new contact records
        public List<Contact> triggerNew;
        // Declare a map variable to store new contact records with their Id as key
        public Map<Id, Contact> triggerNewMap;
        // Declare a list variable to store old contact records
        public List<Contact> triggerOld;
        // Declare a map variable to store old contact records with their Id as key
        public Map<Id, Contact> triggerOldMap;
        // Constructor to initialize the variables
        public ContactData(Map<String, Object> contactsData) {
            // Assign the new contact records to the triggerNew variable
            this.triggerNew = (List<Contact>) contactsData.get('triggerNew');
            // Assign the new contact records with their Id as key to the triggerNewMap variable
            this.triggerNewMap = (Map<Id, Contact>) contactsData.get('triggerNewMap');
            // Assign the old contact records to the triggerOld variable
            this.triggerOld = (List<Contact>) contactsData.get('triggerOld');
            // Assign the old contact records with their Id as key to the triggerOldMap variable
            this.triggerOldMap = (Map<Id, Contact>) contactsData.get('triggerOldMap');
        }
    }
}

Test Class:
@isTest
private class UpdateContactCountOnAccountHandlerTest {
  @TestSetup
  static void createTestData() {
    // Create Accounts
    Account acc1 = new Account(Name = 'Test Account 1');
    Account acc2 = new Account(Name = 'Test Account 2');
    insert new List<Account>{ acc1, acc2 };
  }
  @isTest
  static void testHandleAfterInsert() {
    Account account = [SELECT Id FROM Account LIMIT 1];
    Contact con1 = new Contact(FirstName = 'Test', LastName = 'Contact 1', AccountId = account.Id);
    Contact con2 = new Contact(FirstName = 'Test', LastName = 'Contact 2', AccountId = account.Id);
    Contact[] contacts = new List<Contact>{ con1, con2 };
    Test.startTest();
    insert contacts;
    Test.stopTest();
    // Verify the expected results
    Account updatedAcc = [
      SELECT Id, Number_of_Contacts__c
      FROM Account
      WHERE Id = :account.Id
    ];
    System.assertEquals(2, updatedAcc.Number_of_Contacts__c, 'Number of contacts is 2');
  }

  @isTest
  static void testHandleAfterUpdate() {
    List<Account> accounts = [SELECT Id FROM Account];
    Contact con1 = new Contact(
      FirstName = 'Test',
      LastName = 'Contact',
      AccountId = accounts[0].Id
    );
    Contact con2 = new Contact(
      FirstName = 'Test',
      LastName = 'Contact',
      AccountId = accounts[1].Id
    );
    Contact[] contacts = new List<Contact>{ con1, con2 };
    insert contacts;

    // Call the trigger handler to update the Account of con1
    con1.AccountId = accounts[1].Id;
    update con1;

    // Verify the expected results
    Account updatedAcc1 = [
      SELECT Id, Number_of_Contacts__c
      FROM Account
      WHERE Id = :accounts[0].Id
    ];
    System.assertEquals(0, updatedAcc1.Number_of_Contacts__c, 'Number of contacts is 0');
    Account updatedAcc2 = [
      SELECT Id, Number_of_Contacts__c
      FROM Account
      WHERE Id = :accounts[1].Id
    ];
    System.assertEquals(2, updatedAcc2.Number_of_Contacts__c, 'Number of contacts is 2');
  }

  @isTest
  static void testHandleAfterDelete() {
    List<Account> accounts = [SELECT Id FROM Account];
    Contact con1 = new Contact(
      FirstName = 'Test',
      LastName = 'Contact',
      AccountId = accounts[0].Id
    );
    Contact con2 = new Contact(
      FirstName = 'Test',
      LastName = 'Contact',
      AccountId = accounts[1].Id
    );
    Contact[] contacts = new List<Contact>{ con1, con2 };
    insert contacts;

    // Call the trigger handler to delete con1 and con2
    delete contacts;
    // Verify the expected results
    Account updatedAcc1 = [
      SELECT Id, Number_of_Contacts__c
      FROM Account
      WHERE Id = :accounts[0].Id
    ];
    System.assertEquals(0, updatedAcc1.Number_of_Contacts__c, 'Number of contacts is 0');
    Account updatedAcc2 = [
      SELECT Id, Number_of_Contacts__c
      FROM Account
      WHERE Id = :accounts[1].Id
    ];
    System.assertEquals(0, updatedAcc2.Number_of_Contacts__c, 'Number of contacts is 0');
    Test.startTest();
    undelete contacts;
    // Verify the expected results
    Account updatedAcc3 = [
      SELECT Id, Number_of_Contacts__c
      FROM Account
      WHERE Id = :accounts[0].Id
    ];
    System.assertEquals(1, updatedAcc3.Number_of_Contacts__c, 'Number of contacts is 1');
    Account updatedAcc4 = [
      SELECT Id, Number_of_Contacts__c
      FROM Account
      WHERE Id = :accounts[1].Id
    ];
    System.assertEquals(1, updatedAcc4.Number_of_Contacts__c, 'Number of contacts is 1');
    Test.stopTest();
  }
}

For more helpful articles please visit – https://thesalesforcedev.in

One response to “Learning Apex Triggers (Part 2) – Trigger Handler Pattern Implementation”

Leave a comment