Apex Triggers in Salesforce

In this article, we will dive into Apex Triggers in Salesforce, making it easy to understand for students with little or no programming experience. The goal is to introduce you to triggers, their uses, and how to write efficient triggers in Salesforce.

What is an Apex Trigger?

An Apex Trigger is a piece of code that runs automatically when specific events occur in Salesforce, such as creating, updating, or deleting a record. Triggers are used when you want Salesforce to perform actions based on these events.

Why Use Apex Triggers?

  • Automating Processes: You can automate tasks, such as updating related records when one record is changed.
  • Enforcing Business Logic: If you want to enforce custom rules that aren’t possible with validation rules, triggers can help.
  • Custom Workflows: You can set up workflows that go beyond what standard Salesforce functionality offers.

Trigger Context Variables

Triggers provide several built-in variables to access the records being processed. Here are some important context variables:

  • Trigger.new: Contains the new versions of the records being inserted or updated.
  • Trigger.old: Contains the old versions of the records being updated or deleted.
  • Trigger.newMap: A map of new records with their IDs as keys.
  • Trigger.oldMap: A map of old records with their IDs as keys.
  • Trigger.isInsert: Returns true if the trigger was fired because of an insert operation.
  • Trigger.isUpdate: Returns true if the trigger was fired because of an update operation.
  • Trigger.isDelete: Returns true if the trigger was fired because of a delete operation.

Types of Apex Triggers

There are two main types of triggers:

  1. Before Triggers
    • These triggers run before a record is saved to the database.Use Case: They are typically used to modify or validate data before it is saved.
    Example Code for Before Trigger: Let’s say we want to ensure that an Account’s phone number is saved in a specific format (like “(123) 456-7890”) whenever an account is created or updated.
trigger FormatAccountPhone on Account (before insert, before update) {
    for (Account acc : Trigger.new) {
        if (acc.Phone != null && acc.Phone.length() == 10) {
            // Format phone number as (123) 456-7890
            acc.Phone = '(' + acc.Phone.substring(0, 3) + ') ' 
                      + acc.Phone.substring(3, 6) + '-' 
                      + acc.Phone.substring(6);
        }
    }
}

Explanation:

  1. Trigger Type: This trigger works for both before insert and before update operations.
  2. Context Variable: Trigger.new is used, which holds the list of new or updated Account records.
  3. Logic:
    • We check if the phone number (acc.Phone) is not null and has exactly 10 digits.
    • If it does, we format it as (123) 456-7890 using the substring method.
    • This formatting happens before the record is saved to the database, ensuring that the phone number is saved in the desired format.

Test Class for Trigger

It’s important to have a test class that verifies the trigger works as expected:

@isTest
public class FormatAccountPhoneTest {
    
    @isTest
    static void testPhoneFormatting() {
        // Create a new Account with a 10-digit phone number
        Account acc = new Account(Name = 'Test Account', Phone = '1234567890');
        insert acc;
        
        // Query the inserted account
        Account insertedAcc = [SELECT Id, Phone FROM Account WHERE Id = :acc.Id];
        
        // Verify that the phone number was formatted correctly
        System.assertEquals('(123) 456-7890', insertedAcc.Phone);
        
        // Update the account with a new phone number
        insertedAcc.Phone = '0987654321';
        update insertedAcc;
        
        // Query the updated account
        Account updatedAcc = [SELECT Id, Phone FROM Account WHERE Id = :insertedAcc.Id];
        
        // Verify that the new phone number was formatted correctly
        System.assertEquals('(098) 765-4321', updatedAcc.Phone);
    }
}

Explanation of the Test Class:

  • We create an Account with a phone number that has 10 digits.
  • After inserting it, we query the Account to verify the phone number was formatted correctly by the trigger.
  • We then update the Account with a new 10-digit phone number and check that it is also formatted correctly after the update.
  1. After Triggers
    • These triggers run after a record has been saved to the database.Use Case: They are used when you need to work with data that is already saved, such as updating related records.
    Example Code for After Trigger: Let’s say we want to update an Account’s total revenue field after an Opportunity related to that Account is either inserted or updated. Whenever an Opportunity is created or its Amount field is updated, we will update the total revenue on the related Account.
trigger UpdateAccountRevenue on Opportunity (after insert, after update) {
    // Create a map to store Account Ids and their related total Opportunity amounts
    Map<Id, Decimal> accountRevenueMap = new Map<Id, Decimal>();

    // Loop through the newly inserted or updated Opportunity records
    for (Opportunity opp : Trigger.new) {
        if (opp.AccountId != null) {
            if (!accountRevenueMap.containsKey(opp.AccountId)) {
                accountRevenueMap.put(opp.AccountId, 0);
            }
            // Add the Opportunity's amount to the total revenue
            accountRevenueMap.put(opp.AccountId, accountRevenueMap.get(opp.AccountId) + opp.Amount);
        }
    }

    // Fetch related Account records
    List<Account> relatedAccounts = [SELECT Id, Total_Revenue__c FROM Account WHERE Id IN :accountRevenueMap.keySet()];

    // Update each Account’s total revenue field
    for (Account acc : relatedAccounts) {
        acc.Total_Revenue__c += accountRevenueMap.get(acc.Id);
    }

    // Perform the DML update on the Accounts
    update relatedAccounts;
}

Explanation of the After Trigger:

  1. Trigger Type: This trigger works after insert and update operations on the Opportunity object.
  2. Trigger Context Variable: We use Trigger.new to access the newly created or updated Opportunity records.
  3. Logic:
    • We first loop through the Opportunity records in Trigger.new and gather their AccountId to calculate the total amount of all related Opportunities.
    • We then fetch the related Accounts using a SOQL query and update their Total_Revenue__c field by adding the summed Opportunity amounts.
    • Finally, we perform the DML update on the related Account records to save the changes.

Test Class for After Trigger

Here’s a test class to verify that the after trigger works as expected:

@isTest
public class UpdateAccountRevenueTest {

    @isTest
    static void testRevenueUpdate() {
        // Create a test Account
        Account testAccount = new Account(Name = 'Tech Corp', Total_Revenue__c = 0);
        insert testAccount;

        // Insert a new Opportunity related to the test Account
        Opportunity opp = new Opportunity(Name = 'New Deal', AccountId = testAccount.Id, Amount = 10000, StageName = 'Prospecting', CloseDate = Date.today());
        insert opp;

        // Query the Account and check the total revenue has been updated
        Account updatedAccount = [SELECT Id, Total_Revenue__c FROM Account WHERE Id = :testAccount.Id];
        System.assertEquals(10000, updatedAccount.Total_Revenue__c);

        // Update the Opportunity amount
        opp.Amount = 20000;
        update opp;

        // Query the Account again and check the total revenue has been updated
        updatedAccount = [SELECT Id, Total_Revenue__c FROM Account WHERE Id = :testAccount.Id];
        System.assertEquals(20000, updatedAccount.Total_Revenue__c);
    }
}

Best Practices for Writing Apex Triggers

To write efficient triggers, follow these best practices:

  1. Bulkify Your Code: Triggers in Salesforce are often invoked for multiple records at once. Write your triggers to handle bulk operations by processing all records in Trigger.new or Trigger.old.
    Example of Bulkified Code:
trigger AccountPhoneFormat on Account (before insert, before update) {
    for (Account acc : Trigger.new) {
        if (acc.Phone != null && acc.Phone.length() == 10) {
            acc.Phone = '(' + acc.Phone.substring(0, 3) + ') ' 
                      + acc.Phone.substring(3, 6) + '-' 
                      + acc.Phone.substring(6);
        }
    }
}
  1. One Trigger per Object: Limit yourself to one trigger per object. You can manage multiple functionalities within a single trigger by using if-else conditions or separate handler methods.
  2. Avoid Hardcoding: Do not hardcode values like record IDs, field values, or URLs. Use custom settings or labels to make your code adaptable across different environments.
  3. Use Helper Classes: Move the business logic out of the trigger and into a separate class (known as a helper or handler class). This makes the code cleaner and easier to maintain.
    Example:
trigger AccountTrigger on Account (before insert) {
    AccountHelper.beforeInsert(Trigger.new);
}

public class AccountHelper {
    public static void beforeInsert(List<Account> newAccounts) {
        for (Account acc : newAccounts) {
            if (acc.Phone != null && acc.Phone.length() == 10) {
                acc.Phone = '(' + acc.Phone.substring(0, 3) + ') ' 
                          + acc.Phone.substring(3, 6) + '-' 
                          + acc.Phone.substring(6);
            }
        }
    }
}
  1. Test Thoroughly: Write unit tests that cover all possible scenarios for your triggers, including bulk operations and error conditions.

Conclusion

Apex Triggers are a powerful tool in Salesforce for automating business processes. By understanding the types of triggers, how to use context variables, and following best practices, you can write efficient and scalable code that enhances your Salesforce applications.