0

I need to update another developer's code that is running up against the Apex CPU limit when dealing with a large number of accounts.

The code itself fired on afterUpdate trigger on the Account standard object. It's used to find the Ultimate parent in the account hierarchy (whether or not the hierarchy changes took place).

    public void recomputeUltimateParents(Set<Id> accts, Set<Account> updates) {
   
    List<Account> accounts = [select Id,
            Parent.Id,
            Parent.Parent.Id,
            Parent.Parent.Parent.Id,
            Parent.Parent.Parent.Parent.Id,
            Parent.Parent.Parent.Parent.Parent.Id,
            Ultimate_Parent__c
    from Account
    where Id in :accts];

    for(Account ac : accounts) {


        String originalUltimate = ac.Ultimate_Parent__c;
        if (ac.Parent.Parent.Parent.Parent.Parent.Id != null) {
            ac.Ultimate_Parent__c = ac.Parent.Parent.Parent.Parent.Parent.Id;
        } else if (ac.Parent.Parent.Parent.Parent.Id != null) {
            ac.Ultimate_Parent__c = ac.Parent.Parent.Parent.Parent.Id;
        } else if (ac.Parent.Parent.Parent.Id != null) {
            ac.Ultimate_Parent__c = ac.Parent.Parent.Parent.Id;
        } else if (ac.Parent.Parent.Id != null) {
            ac.Ultimate_Parent__c = ac.Parent.Parent.Id;
        } else if (ac.Parent.Id != null) {
            ac.Ultimate_Parent__c = ac.Parent.Id;
        } else {
            ac.Ultimate_Parent__c = null;
        }

        // IF a legit change took place (we ONLY want to include records that actually changed for updates because of some validation rule issues)
        if (ac.Ultimate_Parent__c != originalUltimate) {
            
            boolean foundInUpdates = false;
            for (Account uac : updates) {
                if (uac.Id == ac.Id) {
                    uac.Ultimate_Parent__c = ac.Ultimate_Parent__c;
                    foundInUpdates = true;
                    break;
                }
            }
            // append to updates IF we had a legit change:
            if (!foundInUpdates) {
                updates.add(ac);
            }
        }
    }
}

It all works fine until some Account has a large number of children. The re-parenting gets stuck in the final loop and runs over the Limits.

I believe my options are:

  1. Moving the second loop into a @future method. The problem with that is future methods don't support neither Maps or Sets. It also doesn't work with Objects and sObjects.

  2. Batch apex.

  3. Queueable Apex

P.S.: As a temporary solution we have a SchedulableContext that does the work. We can schedule it to run daily (or as needed).

Any ideas?

Thanks!

2

Depending on how deep you expect your hierarchy to be, you can write two formulas:

SuperParent__c (Return Type Text):

BLANKVALUE(Parent.Parent.Parent.Parent.ParentId,
BLANKVALUE(Parent.Parent.Parent.ParentId,
BLANKVALUE(Parent.Parent.Parent.Id,
BLANKVALUE(Parent.Parent.Id,
BLANKVALUE(Parent.Id, null)))))

ExtendedSuperParent__c (Return Type Text):

BLANKVALUE(Parent.Parent.Parent.Parent.SuperParent__c,
BLANKVALUE(Parent.Parent.Parent.SuperParent__c,
BLANKVALUE(Parent.Parent.SuperParent__c,
BLANKVALUE(Parent.SuperParent__c,
BLANKVALUE(SuperParent__c, null)))))

This gives you ten levels of hierarchy calculated automatically by the database, changing your code to just:

public void recomputeUltimateParents(Set<Id> accts, Set<Account> updates) {
    Account[] accounts = [SELECT ExtendedSuperParent__c FROM Account WHERE Id = :accts];
    for(Account ac : accounts) {
        String originalUltimate = ac.Ultimate_Parent__c;
        ac.Ultimate_Parent__c = ac.ExtendedSuperParent__c;
        // IF a legit change took place (we ONLY want to include records that actually changed for updates because of some validation rule issues)
        if (ac.Ultimate_Parent__c != originalUltimate) {
            boolean foundInUpdates = false;
            for (Account uac : updates) {
                if (uac.Id == ac.Id) {
                    uac.Ultimate_Parent__c = ac.Ultimate_Parent__c;
                    foundInUpdates = true;
                    break;
                }
            }
            // append to updates IF we had a legit change:
            if (!foundInUpdates) {
                updates.add(ac);
            }
        }
    }
}

You can then use this formula to extract the top level account from there if you want to gather another ten levels of hierarchy.

This should solve both your CPU time and code complexity.

Note that I presume ten levels of hierarchy are enough. If you need more, you just need to implement this recursively.

Edit: The second parameter really should be a map to avoid the nested iteration:

public void recomputeUltimateParents(Set<Id> accts, Map<Id, Account> updates) {
    Account[] accounts = [SELECT ExtendedSuperParent__c FROM Account WHERE Id = :accts];
    for(Account ac : accounts) {
        String originalUltimate = ac.Ultimate_Parent__c;
        ac.Ultimate_Parent__c = ac.ExtendedSuperParent__c;
        // IF a legit change took place (we ONLY want to include records that actually changed for updates because of some validation rule issues)
        if (ac.Ultimate_Parent__c != originalUltimate) {
            Account accountUpdate = updates.get(ac.Id);
            if(accountUpdate != null) {
                uac.Ultimate_Parent__c = ac.Ultimate_Parent__c;
            } else {
                updates.put(ac.Id, ac);
            }
        }
    }
}
7
  • Hey @sfdcfox , Thanks for jumping on! I'm using the code you posted and running into the same CPU limit exceeded issue. Just for reference, I need to re-parent about 3000 accounts at the time. Help me understand here. This solution improves the code just a little bit. With the ExtendedSuperParent__c I don't avoid any loops, which is probably why I hit the limits. Right now I'm exploring the possibility of using the if (!system.isBatch() && !system.isFuture()) . When it's a batch, I will run a public class ... implements Queueable . In the else I will call a method that is NOT a batch.
    – Slava
    Jan 14 at 23:04
  • @Slava Well, thinking about it, I was keeping with the current API signature of your method. Ideally, the Set<Account> updates should be a Map<Id, Account>, instead. I didn't want to assume too much. That said, I'll edit this. ... done. Does that help at all?
    – sfdcfox
    Jan 14 at 23:31
  • 1
    Wouldn't creating the formula field use up a bunch of spanning references? help.salesforce.com/s/articleView?id=000315562&type=1 10 is a lot to use for one use case considering you can only have a max of 20 per object. Jan 14 at 23:53
  • 2
    Ah, you're right, It counts as 4, not 10: Parent, Parent.Parent, Parent.Parent.Parent, Parent.Parent.Parent.Parent, Jan 15 at 0:35
  • 1
    I found that by creating the two fields you recommended and then creating a crazy formula that I knew would put me over the limit. When trying to save, you get an error message that shows you all the references. Jan 15 at 0:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.