5

I have a question relating to the best practices when coding a trigger.

I have already read up a fair bit on the best practices and understand the one trigger per object pattern, as well as the helper class pattern to move logic away from the trigger.

My question is - what should we do when we have multiple actions for an object where each of these actions affects multiple other objects?

Example: Using Case, Account and a custom object OBJECTABC.

OBJECTABC has a reference to Account but not to Case. There can be many OBJECTABC per Account.

We have the following two business rules that need to be implemented using a trigger:

SET LAST CASE UPDATED ON ACCOUNT AND OBJECTABC

Whenever a Case of type "FEEDBACK" is inserted or updated, it should also set the field "LastCaseUpdated" on the cases Account to the date that the Case was updated. It should also set the field "LastCaseUpdated" on OBJECTABC.

INCREASE THE CASE COUNT ON ACCOUNT AND OBJECTABC

Whenever a Case is inserted and it is of type "ENQUIRY", it should also increase the field "NumberOfCases" on the cases Account by one. It should also increase the field "NumberOfCases" on OBJECTABC.

SO WHICH PATTERN TO USE?

I would like to retrieve the Account and the OBJECTABC records only once from the database using SOQL to reduce the number of SOQL calls. I would also like to only call UPDATE once for each of these objects.

Would the best practice be to have one handler class for Case, with methods for "SetLastUpdated" and "IncreaseCaseCount"? Would I then have collections for Account and OBJECTABC that I ensure are only loaded on the first trigger call, and then subsequent calls affect the collections instead of retrieving the data again? Issues I see with this is that it could become a very, very large messy handler class.

Would the best practice be to have separate handler classes for Account and OBJECTABC, each with their own methods for "SetLastUpdated" and "IncreaseCaseCount"? This would then mean duplicating the logic for each object (e.g. I'd need to check the case type for each of these in my example, but the logic that would need to be duplicated could be much more complex).

Would the best practice be to have separate handler classes for "SetLastUpdated" and "IncreaseCaseCount"? I could then keep the logic contained, but then how would I handle loading the Account and OBJECTABC only once, and how would I pass this around from handler to handler so that I only retrieve and update the objects once?

Apologies for the long question, and I look forward to your assistance :)

2 Answers 2

1

Here's my opinion on what "best practice" is for trigger handlers.

  • Your Trigger should only answer when questions.
    • What event is firing?
  • Your Handler should only answer what/which questions.
    • What operation(s) to perform?
    • Which records to act on (criteria)?
  • Your Service should only answer how questions.
    • How to perform each action?
    • How to apply each filter (criterion)?

Each layer will pass stateful information (the trigger records) down as needed. In my observation, the term Helper class is not used rigorously as these terms above.

In your scenario, all your logic can be written in a CaseService class. Something like the following:

public with sharing class CaseService
{
    public static void updateChildAccounts(List<Case> records)
    {
        Set<Id> accountIds = new Set<Id>();
        for (Case record : cases) accountIds.add(record.AccountId);

        List<SObject> recordsToUpdate = new List<SObject>();
        for (Account child : [
            SELECT Id, (SELECT Id FROM ABCs__r) FROM Account
            WHERE Id IN :accountIds
        ]){
            // set account fields
            recordsToUpdate.add(child);

            for (ABC__c grandchild : child.ABCs__r)
            {
                // set ABC__c fields
                recordsToUpdate.add(grandchild);
            }
        }

        recordsToUpdate.sort();
        update recordsToUpdate;
        // error handling strongly recommended, but omitted here for brevity
    }
}

There's a lot to unpack about how the above was written to consume just one query and one dml operation. About the query limits, you did consume a second query by getting the children, but this type of sub-query consumes a separate governor limit. Usually this "aggregate query" limit is not one you have to worry about overly much.

The next thing to understand is that you can Create Records for Different Object Types. You can insert up to ten different types, but if you alternate back and forth between Account and ABC__c, each chunk counts towards that maximum. That's where sort comes in, and thankfully the first step in the sort sequence is to check the type of sObject. So after you call sort you're back down to two chunks and you're good to go!

As for exception handling, you should read up on how to best handle a DmlException. I'm having a surprisingly hard time finding any good resources to link at the moment, but I'll try to come back and add it in if I find one.

My basic pattern for the rest would look like:

Trigger

trigger CaseTrigger on Case (before insert)
{
    CaseTriggerHandler handle = new CaseTriggerHandler(trigger.new, trigger.oldMap);
    if (trigger.isBefore)
    {
        if (trigger.isInsert) handle.beforeInsert();
        if (trigger.isUpdate) handle.beforeUpdate(); // if you needed it
    }
    if (trigger.isAfter) // if you needed it
    {
        // etc.
    }
}

Handler

public with sharing class CaseTriggerHandler
{
    @TestVisible static Boolean bypassTrigger = false;
    final List<Case> newRecords;
    final Map<Id, Case> oldMap;
    public CaseTriggerHandler(List<Case> newRecords, Map<Id, Case> oldMap)
    {
        this.newRecords = newRecords;
        this.oldMap = oldMap;
    }

    public void beforeInsert()
    {
        if (bypassTrigger) return;

        CaseService.updateChildAccounts(newRecords);
    }
    public void afterInsert() { /*if needed*/ }

    public void beforeUpdate() { /*if needed*/ }
    // etc.
}
0

One helper Class per Object is the norm.

However it is all about maintainability.

For example you probably want to see all logic in the same screen while building and maintaining. So yes 1 trigger with methods within the same helper class would make sense. One tiny reason to seperate each business process would be if they needed there own files for the purpose of a larger team maintaining them each in isolation.

Processing... You should query all objects first iterate over initial data to map and prepare. query again if you have to. iterate over total data manipulate/process the data from your findings. Then do your dml.

If you need to prevent triggers from running again (recursive), then use a separate trigger handler with booleans like "runOnce" to prevent recursive activity.

Based on your posted business logic, you should be able to do 1 SOQL query, but it will be measured like 2 queries anyway, then iterate across Account and OBJECTABC with a nested for loop.

allCases = [SELECT id, subject FROM case WHERE accountId IN AccountId_Set];
for(case c : cases ){
    case_Map.put(accountId, c);
}
accounts = [SELECT id, name, LastCaseUpdated, NumberOfCases, (SELECT id, name, LastCaseUpdated, NumberOfCases FROM OBJECTABCs) FROM Account WHERE Id IN :CaseId_Set];

For(Account a: accounts){
    //code here referring to case_Map.get(a.Id) to build you variables for all business processes.
    for(OBJECTABC abc :a.OBJECTABCs){
       //code here refferring to 
       abc_List.add(abc);
    }
    // code here
    account_List.add(a);
}
update account_List;
update abc_List;

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .