1

We Have a certain Account Hierarchy designed in out Org.It display like this:

   Account(Top Account)
    ---Child Account 1
       ----Child Account 1.1
             ----Child Account 1.1.1
                 ----Child Account 1.1.1.1

We can add any number of child accounts to Top Accounts but within an accounts we can only add 5 accounts.

Whenever we add multiple child accounts to Top Account,we update Classficiation values of those child accounts by Top Account Classification values.

Below are the classification Field:

AccountClassification1__c,AccountClassification2__c ......AccountClassification7__c

each classification field is of type picklist and contains value as 1,2,3,4.

Now If Top Account Classification value for AccountClassification1__c,AccountClassification2__c,AccountClassification3__c,AccountClassification4__c is 1,2,3,4 then the same classification value is reflected for all child accounts. It does not matter how many child accounts it has. If the Classification field does not have any value, it will be updated with a blank value.

Below are the code that updates classfication Value:

public with sharing class AccountClassificationServices {

    public static Boolean hasClassficationUpdated(Account account, Account oldAccount) {

        List<Account> changedAccounts = new List<Account>();

        // This stops the loops where classification for children is updated
        if (account.ParentId <> null) {
            return false;
        }

        if (oldAccount == null
            || account.AccountCategory__c <> oldAccount.AccountCategory__c
            || account.Account_Subcategory__c <> oldAccount.Account_Subcategory__c
            || account.AccountClassification1__c <> oldAccount.AccountClassification1__c
            || account.AccountClassification2__c <> oldAccount.AccountClassification2__c
            || account.AccountClassification3__c <> oldAccount.AccountClassification3__c
            || account.AccountClassification4__c <> oldAccount.AccountClassification4__c
            || account.AccountClassification5__c <> oldAccount.AccountClassification5__c
            || account.AccountClassification6__c <> oldAccount.AccountClassification6__c
            || account.AccountClassification7__c <> oldAccount.AccountClassification7__c
           )

            return false;

    }
    public static List<Account> updateClassification(Map<Id,Account> accMap) {
        List<Account> accList = findAccountToUpdate(accMap);
        // find ultimate accounts with classification fields
        Set<Id> utimateIds = new Set<Id>();
        for (Account acc: accList) {
            utimateIds.add(acc.UltimateParent__c);
        }
        Map<Id,Account> ultimateMap = new Map<Id,Account>(new SelectAccounts().execute(utimateIds));

        // update statistics from ultimates
        List<Account> accToUpdate = new List<Account>();
        for (Account acc: accList) {
            Account parentAccount = ultimateMap.get(acc.UltimateParent__c);
            accToUpdate.add(new Account(
                Id = acc.Id,
                AccountCategory__c = parentAccount.AccountCategory__c,
                Account_Subcategory__c = parentAccount.Account_Subcategory__c,
                AccountClassification1__c = parentAccount.AccountClassification1__c,
                AccountClassification2__c = parentAccount.AccountClassification2__c,
                AccountClassification4__c = parentAccount.AccountClassification4__c,
                AccountClassification5__c = parentAccount.AccountClassification5__c,
                AccountClassification6__c = parentAccount.AccountClassification6__c,
                AccountClassification7__c = parentAccount.AccountClassification7__c,    
                AllowClassificationUpdate__c = true,
                Allow_CategorySubCategory_Update__c = true
            ));
        }

        // update classification and reset flag
        (new UpdateChildren()).execute(accToUpdate);
        resetAllowClassificationUpdate(accToUpdate);
        resetAllowCategoryUpdate(accToUpdate);

        return accToUpdate;
    }
    private static List<Account> findAccountToUpdate(Map<Id,Account> accMap) {
        Map<Id,Account> updateMap = new Map<Id,Account>();
        Set<Id> utimateIds = new Set<Id>();
        // split map by two types
        for (Id accId: accMap.keySet()) {
            Account acc = accMap.get(accId);
            if ( acc == null ) {
                // whole hierarchy update
                utimateIds.add(accId);
            } else {
                // single record
                updateMap.put(acc.Id, acc);
            }
        }

        if ( !utimateIds.isEmpty() ) {
            // find all children for hierarchy
            for (Account acc: getAllChildrenByUltimateParent(utimateIds)) {
                updateMap.put(acc.Id, acc);
            }
        }

        return updateMap.values();
    }

    public static List<Account> getAllChildrenByUltimateParent(Set<Id> ultimateParentIds) {
        return [
            select
            Id,RecordTypeId,AccountCategory__c, Account_Subcategory__c,UltimateParent__c, AccountClassification1__c, AccountClassification2__c, AccountClassification3__c,
            AccountClassification4__c, AccountClassification5__c, AccountClassification6__c, AccountClassification7__c, AllowClassificationUpdate__c, Allow_CategorySubCategory_Update__c,
            BillingCountry, BillingCountryCode
            from Account
            where UltimateParent__c IN :ultimateParentIds and ParentId <> null
        ];
    }

    public static Boolean hasAllowClassification(Account account) {
        return account.AllowClassificationUpdate__c == true;
    }

    public static Boolean hasAllowCategory(Account account) {
        return account.Allow_CategorySubCategory_Update__c == true;
    }

    public static void resetAllowClassificationUpdate(List<Account> accounts) {
        // accounts = new SelectAccounts().execute(accounts);

        for (Account account : accounts) {
            account.AllowClassificationUpdate__c = false;
        }

        (new UpdateChildren()).execute(accounts);
    }

    public static void resetAllowCategoryUpdate(List<Account> accounts) {
        // accounts = new SelectAccounts().execute(accounts);

        for (Account account : accounts) {
            account.Allow_CategorySubCategory_Update__c = false;
        }

        (new UpdateChildren()).execute(accounts);
    }

    @TestVisible
    private without sharing class SelectAccounts
    {

        public List<Account> execute(Set<Id> accIds)
        {
            return [select
                    ParentId,RecordTypeId,AccountCategory__c, Account_Subcategory__c,UltimateParent__c, AccountClassification1__c, AccountClassification2__c, AccountClassification3__c,
                    AccountClassification4__c, AccountClassification5__c, AccountClassification6__c, AccountClassification7__c, AllowClassificationUpdate__c, Allow_CategorySubCategory_Update__c,
                    BillingCountry, BillingCountryCode
                    from Account
                    where Id IN :accIds];
        }
    }
    private without sharing class UpdateChildren
    {
        public void execute(List<Account> children)
        {
            if (!children.isEmpty()) {
                update children;
            }
        }
    }
}

Below is the Trigger Code:

public with sharing class AccountHandler implements ITrigger {

    private List<SObject> accountsWithParentBeforeInsert = new List<SObject>();

    private List<Account> accountsReceiveParents = new List<Account>();
    private List<Account> accountsLostParents = new List<Account>();

    private List<SObject> accountsAfterInsert = new List<SObject>();
    private List<SObject> accountsAfterUpdate = new List<SObject>();
    private List<SObject> accountsAfterDelete = new List<SObject>();
    private List<SObject> accountGroupsAfterDelete = new List<SObject>();

    private Map<Id, Account> ultimateAccountClassificationChanged = new Map<Id, Account>();

    private Map<Id, Account> ultimateAccountCategoryChanged = new Map<Id, Account>();

    private List<Account> accountsHasClassificationSet = new List<Account>();

    private List<Account> accountsHasCategorySet = new List<Account>();

    public AccountHandler() { }

    public void bulkBefore() { }

    public void bulkAfter() { }

    public void beforeInsert(SObject so) {

    }

    public void beforeUpdate(SObject oldSo, SObject so) {
        Account accountToUpdate = (Account)so;
        Account accountBeforeUpdate = (Account)oldSo;
    }

    public void beforeDelete(SObject so) {
        if (AccountServices.isAccountLocked()) {
            so.addError(Label.AG_Account_Locked);
        }
    }

    public void afterInsert(SObject so) {
        Account acc = (Account)so;

        if ( acc.ParentId != null ) {
            ultimateAccountClassificationChanged.put(acc.Id, acc);
            ultimateAccountCategoryChanged.put(acc.Id, acc);
        }
    }

    public void afterUpdate(SObject oldSo, SObject so) {
        Account accountToUpdate = (Account)so;
        Account accountBeforeUpdate = (Account)oldSo;
        if (accountToUpdate.UltimateParent__c <> accountBeforeUpdate.UltimateParent__c && accountToUpdate.UltimateParent__c <> null ) {
            ultimateAccountClassificationChanged.put(accountToUpdate.Id, accountToUpdate);
            ultimateAccountCategoryChanged.put(accountToUpdate.Id, accountToUpdate);
        } else if (AccountClassificationServices.hasClassficationUpdated(accountToUpdate, accountBeforeUpdate) && accountToUpdate.ParentId == null) {
            ultimateAccountClassificationChanged.put(accountToUpdate.Id, null);
            ultimateAccountCategoryChanged.put(accountToUpdate.Id, null);
        }
    }

    public void afterDelete(SObject so) {
        Account acc = (Account)so;

        accountsAfterDelete.add(acc);
    }

    public void andFinally() {
        // Ultimate Parents
        if (!accountsWithParentBeforeInsert.isEmpty()) {
            AccountHierarchyServices.populateUltimateParents(accountsWithParentBeforeInsert);
        }
        if (!accountsReceiveParents.isEmpty()) {
            AccountHierarchyServices.updateUltimateParents(accountsReceiveParents, false);
        }
        if (!accountsLostParents.isEmpty()) {
            AccountHierarchyServices.updateUltimateParents(accountsLostParents, true);
        }

        if (!ultimateAccountClassificationChanged.isEmpty()) {
            AccountClassificationServices.updateClassification(ultimateAccountClassificationChanged);
        }

        if (!ultimateAccountCategoryChanged.isEmpty()) {
            AccountClassificationServices.updateClassification(ultimateAccountCategoryChanged);
        }
    }
}

Lets say I have top Account as TopA and has 10 child accounts. There is another Top Account 'TopB' which has 250 Child Accounts. If I want to make 'TopB' as a child to 'TopA' then it executes above code and throws an 'Apex CPU time limit exceeded' exception. Because it updates the TopB and its child accounts Classfication value with TopA classfication value.

The exception occurs when we have a large Account Hierarchy as I mentioned in the example.

The exception occurs on After Update events during the below condition

if (accountToUpdate.UltimateParent__c <> accountBeforeUpdate.UltimateParent__c && accountToUpdate.UltimateParent__c <> null ) {
    ultimateAccountClassificationChanged.put(accountToUpdate.Id, accountToUpdate);
    ultimateAccountCategoryChanged.put(accountToUpdate.Id, accountToUpdate);
}

Because it calls AccountClassificationService class and trys to update classification values.

Can someone guide me on what needs to be modified in the above code?

Update:

I have written a batch class in order to resolve this CPU Timeout error. Below is the code that i have written.

   global class batchAccountUpdate implements Database.batchable<sObject>{ 
   public String query;
   global database.querylocator start(Database.BatchableContext info){
   system.debug('query Result:'+query);
       String query1='select Id,ParentId,RecordTypeId,UltimateParent__c,AccountCategory__c,Account_Subcategory__c ,AccountClassification1__c, AccountClassification2__c, AccountClassification3__c,AccountClassification4__c, AccountClassification5__c, AccountClassification6__c, AccountClassification7__c, AllowClassificationUpdate__c,Allow_CategorySubCategory_Update__c,BillingCountry,BillingCountryCode from Account where Id='+query+'';
   system.debug('Query1 Result:'+query1);
   system.debug('Start Info:'+info);
    return Database.getquerylocator(query);
    }     
   global void execute(Database.BatchableContext info, List<Account> children){

       if (!children.isEmpty()) {
                update children;
            }
   }     
   global void finish(Database.BatchableContext info){     
   } 
}

Below is the method where I am calling the Batch CLass.I am not scheduling the batch class as I want to fire it only on specific condition i.e.During Accounts Re parenting or updating Classification value on Top Account.

public static List<Account> updateClassification(Map<Id,Account> accMap) {
List<Account> accList = findAccountToUpdate(accMap);
// find ultimate accounts with classification fields
Set<Id> utimateIds = new Set<Id>();
for (Account acc: accList) {
utimateIds.add(acc.UltimateParent__c);
}
Map<Id,Account> ultimateMap = new Map<Id,Account>(new SelectAccounts().execute(utimateIds));
SET<ID> keys = ultimateMap.keyset();

// update statistics from ultimates
List<Account> accToUpdate = new List<Account>();
for (Account acc: accList) {
Account parentAccount = ultimateMap.get(acc.UltimateParent__c);
accToUpdate.add(new Account(
Id = acc.Id,
AccountCategory__c = parentAccount.AccountCategory__c,
Account_Subcategory__c = parentAccount.Account_Subcategory__c,
AccountClassification1__c = parentAccount.AccountClassification1__c,
AccountClassification2__c = parentAccount.AccountClassification2__c,
AccountClassification4__c = parentAccount.AccountClassification4__c,
AccountClassification5__c = parentAccount.AccountClassification5__c,
AccountClassification6__c = parentAccount.AccountClassification6__c,
AccountClassification7__c = parentAccount.AccountClassification7__c,    
AllowClassificationUpdate__c = true,
Allow_CategorySubCategory_Update__c = true
));
}
string query= '\''+String.join(keys,'\',\'')+'\'';

//Calling Batch Class
batchAccountUpdate execbatch = new batchAccountUpdate();
execbatch.query = query;
database.executebatch(execbatch);
resetAllowClassificationUpdate(accToUpdate);
resetAllowCategoryUpdate(accToUpdate);

return accToUpdate;
}

The above code does not occur CPU Timeout issue but it does display script exception message: "System.QueryException: unexpected token: (Id)".

What I have modified in Apex class is I am adding a Dynamic SOQL query and pass it to the Batch Apex class as I need a filter that recognize to fire Batch Job only on certain condition but every time when I do re parenting or Updates Classification value on Top Accounts,it throws an error message.I am not sure what is wrong in my method as I am using correct syntax but it does not recognize filter criteria 'Keys'and throws me an exception.

Can someone please help me what I am doing wrong here?

  • Comments are not for extended discussion; this conversation has been moved to chat. – Adrian Larson Apr 3 '17 at 15:39
  • You really should have asked a separate question for the QueryException. This situation is a bit of a mess... – Adrian Larson Apr 10 '17 at 14:08
6

As noted in my comments, Future methods will allow your trigger use asynchronous classes that will have higher limits to process your data. You'll want to be aware of the following:

Methods with the future annotation must be static methods, and can only return a void type. The specified parameters must be primitive data types, arrays of primitive data types, or collections of primitive data types. Methods with the future annotation cannot take sObjects or objects as arguments.

The reason why sObjects can’t be passed as arguments to future methods is because the sObject might change between the time you call the method and the time it executes. In this case, the future method will get the old sObject values and might overwrite them. To work with sObjects that already exist in the database, pass the sObject ID instead (or collection of IDs) and use the ID to perform a query for the most up-to-date record. The following example shows how to do so with a list of IDs.

This means you'll want to pass the info from trigger.new as a list of strings or Ids. There's a workaround for the limitation on passing objects by using JSON Parsing to first serialize the objects from within your trigger, then deserialize them back into objects in your @future method before processing thee records. Which to do depends on whether you need more than the recordId in order to process record being updated.


There's an alternative approach that can also be taken. That method involves using a custom object to temporarily write your data as a string to the custom object. What you would do is create a string containing the recordId, a separator, and any fields that are necessary to process the record. As in:

'AccountId'+'AccountName'+'NewUltimateParentId'+'OldUltimateParentId'; // or 'None' if null

Use your trigger to write your data to a temporary object named AccountClassificationUpdates__c. create another afterInsert trigger on AccountClassificationUpdates__c which fires a queueable containing the essence of the logic in your service class. That queueable queries all records in the custom object, if any are found (remember that each will have an Id for the custom object), parses them to get the Id and other fields for the Account records and processes them in the queuable class. On success, it deletes the related records in the custom object.

If any records fail, the queueable can "mark" those records with the DML error message (append the error to your string) and retry them a 2nd time by either scheduling itself the queueable to run again, or allowing the record to be processed the next time the trigger calls another instance. If the records fail a 2nd time (the class will know by the number of appended error strings), it can call a class to send an email to the admin for manual intervention along with the recordId and the entire string that includes the string with the accountId, other fields, and error messages received from both processing attempts. It does not delete the record; leaving that to the system admin. It should also mark the record so your queueable knows not to attempt to process it again.

To me, this is a very reliable means of handling asynchronous processing of records in terms of error handling and accounting for records that don't get processed for one reason or another. Whether or not to use this pattern depends on how mission critical these records are to your organization.

EDIT:

From the code you've posted, you are passing in the name of your set<Id>keys as part of a string. I'll add that it doesn't appear that you've set this up as an @future method, but presumably there's code you've not posted. You should be able to iterate on keys within a string.join method. If not, you may need to create a new list<Id>keylist and use keylist.addAll(keys) to get all of the Ids into a list before performing the join as below:

String skeys = String.join(keys, ',');
// puts all the Id values into a comma separated string
String query='select Id,ParentId,RecordTypeId,UltimateParent__c,AccountCategory__c,Account_Subcategory__c ,AccountClassification1__c, AccountClassification2__c, AccountClassification3__c,AccountClassification4__c, AccountClassification5__c, AccountClassification6__c, AccountClassification7__c, AllowClassificationUpdate__c,Allow_CategorySubCategory_Update__c,BillingCountry,BillingCountryCode from Account where Id IN';
query+=':'+skeys; 

That should pass all of the values from the keyset into your query.

EDIT 2:

string query= '\''+String.join(keys,'\',\'')+'\'';

Your query exception is because you're not converting the values in your keyset from Ids to strings as per the code I posted in my previous edit above. You can't add Ids to the end of your querystring and still have a string.

Having a '\' followed by a ' makes it appear to me as though you're trying to escape the first quote, after which you're trying to add the results of the string.join() method to which you then add another escaped single quote. This amounts to the following:

string query= '+string.join()+';

The beginning and ending quotes aren't needed since the results of string.join() should be a string. For some reason that's unclear to me, you appear to also be attempting to escape the separator specified in the join method when you use '\',\''. I'd have expected a compile error here or else the join method to try to use the \ as the separator. All that's needed is ',' to separate the values with commas which is what you want or a ', ' to get a comma followed by a space (the space is unnecessary).

Bottom line, if for some reason the join method isn't converting the list of Ids to a single string correctly, I recommend you create your original map as a <string,Account> so the map's keyset will then be a set<string>. All of the additional escapes you've added are unnecessary.

  • N.B. Queueables can be passed arbitrary types (not just primitives like @future), that said, passing IDs is best as you noted. As for the custom object holding the 'parameters', a serialized JSON String can be used to advantage as the caller/receiver can share the same inner classes – cropredy Apr 5 '17 at 3:35
  • @crmprogdev Currently I am trying to solve this issue via Batch Apex Class as adding future method in class has lots of impact and needs more modification.I have added a batch apex class & 'UpdateClassfication' method of AccountClassificationServices Class.Somehow I am able to resolve the CPU Timeout error but evertime when I do reparenting or updating Top Accounts,it throws an exception message.Could you please look in description and suggest. – Dhananjay Patil Apr 5 '17 at 9:21
  • @DhananjayPatil It appears to me that Set<Id> keys is not defined in your Batch class. All the other variables in the query string appear to be standard variables that are in the schema, but keys is not & does not get passed to the batch class. That's what you're trying to run your query string against. – crmprogdev Apr 5 '17 at 13:11
  • @cropredy I have used the latter pattern to best advantage when there are multiple triggers involved while doing apex managed sharing on multiple related objects. Thus the reason for using strings as opposed to separate fields. – crmprogdev Apr 5 '17 at 13:14
  • @crmprogdev Set<Id> keys is actaully pass in batch class but Database.getQueryLocator() consider it as a string,it does not pass an actual value which is the reason I am getting this error.I have tried the same in anonymous block if I use Database.Query() instead of query locator,it actually pass Id but problem is Batch Class Start method returns only 2 values Iterable & database.querylocator. I not sure how to use Database.query in Start method. – Dhananjay Patil Apr 5 '17 at 13:16

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.