1

So as a child to account, we have an object called Account_Designation__c, which is a junction object between Account and Designation__c (a table of designations such as MBA, CFE, CPA, etc.).

What I am trying to accomplish in a trigger is whenever a new Account_Designation__c is inserted or updated, it queries all of the children Account_Designation__c of the parent Account, updates their fields, then updates the account. However, I get SELF_REFERENCE_FROM_TRIGGER errors because when we update all of the children Account_Designation__c, the trigger fires on them which will cause an update to the original Account_Designation__c. How can I prevent this?

I do have a custom setting that enables/disables triggers, but turning it off then on after the DML for this case seems...unstable.

EDIT: wanted to clarify that I have the trigger working fine on insert, it is the update trigger that is causing me errors.

trigger code

trigger accountDesignationTrigger on Account_Designation__c (before insert, before update) {

Trigger_Settings__c ts = Trigger_Settings__c.getValues('accountDesignationTrigger');
if (ts.Active__c)
{
    if (trigger.isInsert)
    {
        accountDesignationTriggerHelper helper = new accountDesignationTriggerHelper();
        helper.updateAccountFullNameInsert(trigger.new);
    }
    else if (trigger.isUpdate)
    {
        accountDesignationTriggerHelper helper = new accountDesignationTriggerHelper();
        helper.updateAccountFullNameInsert(trigger.new);
    }
}  

trigger helper class

public class accountDesignationTriggerHelper {

public static Boolean isFirstTime = true;

public accountDesignationTriggerHelper(){}

public void updateAccountFullNameInsert(List<Account_Designation__c> triggerNew)
{
    List<Account_Designation__c> finalAccountDesignationUpdateList = new List<Account_Designation__c>();
    List<Account> finalAccountUpdateList = new List<Account>();

    Set<Account_Designation__c> triggerNewSet = new Set<Account_Designation__c>(triggerNew);
    system.debug('triggerNewSet ' + triggerNewSet);
    Set<Id> accIds = new Set<Id>();
    for (Account_Designation__c ad : triggerNew)
    {
        accIds.add(ad.Account__c);
    }

    // get ALL account designations for the affected accounts
    List<Account_Designation__c> accDesignationList = [select id, name, account__c, designation__c, designation_name__c, designation_type__c, order__c
                                                       from account_designation__c where account__c in: accIds];
    accDesignationList.addAll(triggerNew);


    system.debug('accDesignationNewRecords ' + triggerNew);
    system.debug('accDesignationList ' + accDesignationList);

    // get ALL affected accounts
    List<Account> accList = [select id, firstname, middlename, lastname, salutation, suffix from Account where id IN: accIds];
    Map<Id, Account> accMap = new Map<Id, Account>(accList);

    // build a map that relates accounts to their list of account designations
    Map<Account, List<Account_Designation__c>> accountToAccountDesignationMap = new Map<Account, List<Account_Designation__c>>();
    for (Account_Designation__c ad : accDesignationList)
    {
        if (accountToAccountDesignationMap.containsKey(accMap.get(ad.Account__c)))
        {
            List<Account_Designation__c> thisList = accountToAccountDesignationMap.get(accMap.get(ad.Account__c));
            thisList.add(ad);
            accountToAccountDesignationMap.put(accMap.get(ad.Account__c), thisList);
        }
        else
        {
            accountToAccountDesignationMap.put(accMap.get(ad.Account__c), new List<Account_Designation__c>{ad});
        }   
    }

    for (Account a : accountToAccountDesignationMap.keySet())
    {
        String fullNameString = '';
        if (!string.isBlank(a.Salutation))
        {
            fullNameString += a.Salutation + ' ';
        }

        if (!string.isBlank(a.FirstName))
        {
            fullNameString += a.FirstName + ' ';
        }

        if (!string.isBlank(a.MiddleName))
        {
            fullNameString += a.MiddleName + ' ';
        }

        if (!string.isBlank(a.LastName))
        {
            fullNameString += a.LastName;
        }            

        if (!string.isBlank(a.Suffix))
        {
            fullNameString += a.Suffix + ', ';
        }
        else
        {
            fullNameString += ', ';
        }

        // Apex Class wrapper AcfeAccountDesignation: used to sort account designations
        List<AcfeAccountDesignation> AcfeAccountDesignationList = new List<AcfeAccountDesignation>();
        List<AcfeAccountDesignation> AcfeAccountDegreeList = new List<AcfeAccountDesignation>();
        AcfeAccountDesignation CFEAccountDesignation;
        Boolean CFEFound = false;

        List<Account_Designation__c> thisList = accountToAccountDesignationMap.get(a);
        system.debug('thisList ' + thisList);
        system.debug('thisList size ' + thisList.size());
        for (Account_Designation__c ad : thisList)
        {
            AcfeAccountDesignation AccountDesignationObject = new AcfeAccountDesignation(ad);

            if(ad.Designation_Name__c == 'CFE'){
                CFEAccountDesignation = AccountDesignationObject;
                CFEFound = true;
            }
            else if (ad.Designation_Type__c == 'Degree')
            {
                AcfeAccountDegreeList.add(AccountDesignationObject);
            }
            else
            {
                AcfeAccountDesignationList.add(AccountDesignationObject);
            }

        }

        AcfeAccountDesignationList.sort();
        AcfeAccountDegreeList.sort();
        Integer order = 1;
        for(AcfeAccountDesignation ad: AcfeAccountDesignationList)
        {
            Account_Designation__c thisAccountDesignation = ad.AccountDesignation;
            if (!triggerNewSet.contains(thisAccountDesignation))
            {
                system.debug('TRIGGERNEWSET DOES NOT CONTAIN ' + order + ': ' + thisAccountDesignation.Id);
                finalAccountDesignationUpdateList.add(thisAccountDesignation);
            }
            thisAccountDesignation.Order__c = order;
            order++;
        }

        order = 1;
        for(AcfeAccountDesignation ad: AcfeAccountDegreeList)
        {
            Account_Designation__c thisAccountDesignation = ad.AccountDesignation;
            if (!triggerNewSet.contains(thisAccountDesignation))
            {
                system.debug('TRIGGERNEWSET DOES NOT CONTAIN ' + order + ': ' + thisAccountDesignation.Id);
                finalAccountDesignationUpdateList.add(thisAccountDesignation);
            }
            thisAccountDesignation.Order__c = order;
            order++;
        }

        Integer designationsAdded = 0;
        if (AcfeAccountDegreeList.size() > 0)
        {
            AcfeAccountDesignation ad = AcfeAccountDegreeList[0];
            fullNameString += ad.AccountDesignation.Designation_Name__c + ', ';
            designationsAdded++;
        }

        if (CFEFound)
        {
            fullNameString += CFEAccountDesignation.AccountDesignation.Designation_Name__c + ', ';
            designationsAdded++;
            if (!triggerNewSet.contains(CFEAccountDesignation.AccountDesignation))
            {
                system.debug('TRIGGERNEWSET DOES NOT CONTAIN : ' + CFEAccountDesignation.AccountDesignation.Id);
                finalAccountDesignationUpdateList.add(CFEAccountDesignation.AccountDesignation);
            }
            CFEAccountDesignation.AccountDesignation.Order__c = null;
        }

        Integer designationListIndex = 0;
        while(designationsAdded < 4 && !AcfeAccountDesignationList.isEmpty() && designationListIndex < AcfeAccountDesignationList.size())
        {
            AcfeAccountDesignation ad = AcfeAccountDesignationList[designationListIndex];
            fullNameString += ad.AccountDesignation.Designation_Name__c + ', ';
            designationListIndex++;
            designationsAdded++;
        }
        fullNameString = fullNameString.substring(0, fullNameString.length() - 2);
        a.Full_Name_With_Designations__c = fullNameString;
        finalAccountUpdateList.add(a);
        system.debug('fullNameString ' + fullNameString);
    }

    for (Account_Designation__c ad : finalAccountDesignationUpdateList)
    {
        system.debug('finalAccountDesignationUpdateList iterator ' + ad.id);
    }
    upsert finalAccountDesignationUpdateList;
    upsert finalAccountUpdateList;
}
  • Do you ensure that your query does not return records which are currently in trigger context? Did you try creating a @future job to do this update after insert/update? Without code or what you've already tried its difficult to provide solutions. – battery.cord Nov 21 '17 at 15:34
  • @battery.cord I updated the OP with the code. I ensure that I am not upserting records that are in trigger.New, but the problem is when I upsert the other child records, the triggers that fire then update the original record in trigger.New. – Kevin N Nov 21 '17 at 15:41
  • I think a @future method targeting all records on the specified account with a clause to ensure the trigger doesn't fire recursively would be a good solution. Ill write up an answer soon. – battery.cord Nov 21 '17 at 16:04
0

I've written up a simple example showing how a @future method can be used to preform a non-recursive update in a trigger. I've kept the details of your schema out of the answer, so you'll have to implement it yourself, but hopefully this serves as a good example!

@future methods run in an asynchronous context, allowing them to access data in their own context. By adding a @future method to the after insert clause of a trigger, you'll add a job which should execute after the trigger has completed its updates, but with the data you passed to the @future method from the trigger.

First, you'll need to obtain your account ids - this is done in my example via a get and a Set<Id> to get a list without duplicate values in it. Then, pass this list over to a static @future method, defined in the class AccountUpdater.

I've included a check to see if the trigger is already in a future context, which should prevent the trigger from adding another @future job while its running the first one. However, this isn't exactly production grade- you'd likely want a variable you can manually set, with defaults based on when you can call a @future method - you'll need to check the docs for this one.

Inside the @future method, you'll need to query for the child records of the accounts passed in, perform some field modifications, and then call an update on those records.

Trigger

trigger SomeTrigger (after insert, after update) {

    Set<Id> accountIds = new Set<Id>(); 

    for (sObject record:Trigger.new) {
        accountIds.add(record.get('AccountId'));
    }

    if (!System.isFuture()) {
        AccountUpdater.Update(accountIds); 
    }

}

@Future method

public class AccountUpdater {

    @future 
    public static void Update(Set<Id> accountIds) {
        List<sObject> records = [SELECT ID FROM sObject WHERE AccountId IN :accountIds];

        // modify fields ...

        update records; 
    }

}

For your implementation, you might be able to modify one of your helper methods to accept an id list, or other primitive variable (you can't call a @future method with complex variable types, such as sObjects, see the docs for a full list). Then, call that in your trigger, wrapped in a context check to make sure you don't add a @future job to the queue while your already in a future context (this will cause an exception).

  • Thanks for the advice! I have implemented your concept of executing the code. However, even in a future method, I get this error from the Apex Jobs page – Kevin N Nov 21 '17 at 18:28
  • Without seeing your updated code, I probably won't be able to help much. From googling around tho, it looks like there are other errors which can report this error code, which makes it a little harder to debug. Can you create a minimalistic example of the code you are using? (Ie, remove a lot of the implementation so I can run it from my dev console on a sandbox, but make sure it still produces the same error) – battery.cord Nov 21 '17 at 19:09
  • Additionally, make sure that you're not trying to update a record in a before context when it has not been saved & has no id (salesforce.stackexchange.com/questions/31866/…) – battery.cord Nov 21 '17 at 19:11
  • Sorry, was updating that comment on the go and stack exchange locked me out of it. Thanks for the advice! I thought about using a batch job for this feature, didn't cross my mind to use a future method! I have implemented your concept of executing the code. After clearing up some issues in my account trigger (this new future method firing another existing future method on account trigger), I believe the code works well! I am still testing all cases, but things look good for now! Also this is my first stack exchange thread after lurking so long! Thanks for the good first experience :D – Kevin N Nov 21 '17 at 20:53
  • If this answer helped you, you should consider accepting it. You should also try to vote on content which helped you. – battery.cord Nov 28 '17 at 15:49

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.