[SalesForce] Self Reference From Trigger error – Recursive Trigger

I have two before Triggers, one on my Project object & the other on Opportunities.

The Project Trigger takes the value from the Project record's field MPM4_BASE__Deadline__c and copies it into the related Opportunity's field Implementation_Revenue__c & the Opportunity trigger copies the updated value from that field into the Opportunity_Imp_Revenue_Com_Date__c field of a record for junction object ProjectxOpp__c which relates Projects to Opportunities.

Lastly, the value from the ProjectxOpp__c's Opportunity_Imp_Revenue_Com_Date__c is rolled up, using a standard rollup field, to the Project record.

When I edit the MPM4_BASE__Deadline__c field on the Project record, it causes an error

execution of BeforeUpdate caused by: System.DmlException: Update failed. SELF_REFERENCE_FROM_TRIGGER, Object (id = a5E7000000008zR) is currently in trigger WE_IMProjCmpltnUp, therefore it cannot recursively update itself: [] Trigger.WE_ProjxOppUp: line 61, column 1: []: Trigger.WE_IMProjCmpltnUp: line 55, column 1.

Line 61 for Trigger WE_ProjxOppUp is update juncRecs; & line 55 for Trigger WE_IMProjCmpltnUp is update updOpps;.

I've seen several, posts about the error & the knowledge article but none seem to explain the cause of my error. Can anyone see a potential cause?

trigger WE_IMProjCmpltnUp on MPM4_BASE__Milestone1_Project__c (before insert, before update) {


//section of code removed, creates validRecordTypeIds list

    Set<Id> projects = new Set<Id>();
    Map<Id,Date> deadlineDates = new Map<Id,Date>();
    List<Opportunity> updOpps = new List<Opportunity>();

    If(Trigger.isInsert){
        for(MPM4_BASE__Milestone1_Project__c p : Trigger.new){
            if(validRecordTypeIds.contains(p.RecordTypeId) && p.MPM4_BASE__Deadline__c != null)
            {
                projects.add(p.Id);
                deadlineDates.put(p.Id, p.MPM4_BASE__Deadline__c);
            }
        }
    }

    If(Trigger.isUpdate){
                for(MPM4_BASE__Milestone1_Project__c p : Trigger.new){
                    if(validRecordTypeIds.contains(p.RecordTypeId) && p.MPM4_BASE__Deadline__c != null)
                    {
                        MPM4_BASE__Milestone1_Project__c oldP = Trigger.oldMap.get(p.Id);

                        if(oldP.MPM4_BASE__Deadline__c != p.MPM4_BASE__Deadline__c){
                            projects.add(p.Id);
                            deadlineDates.put(p.Id, p.MPM4_BASE__Deadline__c);
                        }
                    }
                }
    }

    if(projects.size() > 0){

        for(ProjectxOpp__c junc : [SELECT Project__c, Opportunity__r.Implementation_Revenue__c FROM ProjectxOpp__c
                                    WHERE Project__c In :projects])
        {
            Opportunity o = junc.Opportunity__r;
            Date newCompDate =  deadlineDates.get(junc.Project__c);
            if(newCompDate > o.Implementation_Revenue__c){
                o.Implementation_Revenue__c = newCompDate;
            }
            updOpps.add(o);
        }
        update updOpps;
    }
}

trigger WE_ProjxOppUp on Opportunity (before update) {

//section of code removed, creates validRecordTypeIds list

    Set<Id> opps = new Set<Id>();
    Map<Id,Date> oppCls = new Map<Id,Date>();
    Map<Id,Decimal> oppNetFSR = new Map<Id,Decimal>();
    Map<Id,Date> oppImpCom = new Map<Id,Date>();
    List<ProjectxOpp__c> juncRecs = new List<ProjectxOpp__c>();

    for(Opportunity o : Trigger.New){
        if(validRecordTypeIds.contains(o.RecordTypeId)){
            Opportunity oldO = Trigger.oldMap.get(o.Id);

            if(oldO.CloseDate != o.CloseDate){
                opps.add(o.Id);
                oppCls.put(o.Id, o.CloseDate);
            }
            if(oldO.Net_Full_Service_Revenue__c != o.Net_Full_Service_Revenue__c){
                opps.add(o.Id);
                oppNetFSR.put(o.Id, o.Net_Full_Service_Revenue__c);
            }               
             if(oldO.Implementation_Revenue__c != o.Implementation_Revenue__c){
                 opps.add(o.Id);
                 oppImpCom.put(o.Id, o.Implementation_Revenue__c);
             }
        }
    }

    if(opps.size() > 0){

        for(ProjectxOpp__c junc : [SELECT Opportunity__c, Opportunity_Net_FSR__c, Opportunity_Close_Date__c, Opportunity_Imp_Revenue_Com_Date__c FROM ProjectxOpp__c
                                    WHERE Opportunity__c IN :opps])
        {
            Date clsDt = oppCls.get(junc.Opportunity__c);
            if(clsDt != null){
                junc.Opportunity_Close_Date__c = clsDt;
            }
            Decimal netFSR = oppNetFSR.get(junc.Opportunity__c);
            if(clsDt != null){
                junc.Opportunity_Net_FSR__c = netFSR;
            }
            Date impRevComDt = oppImpCom.get(junc.Opportunity__c);
            if(impRevComDt != null){
                junc.Opportunity_Imp_Revenue_Com_Date__c = impRevComDt;
            }
            juncRecs.add(junc);
        }
        update juncRecs;
    }
}

Best Answer

The cause of the issue was my Project roll-up fields, which were rolling up values from the ProjxOpp junction object and would have caused the triggers on the Project to execute again.

But interestingly, in this instance Salesforce seems to have been anticipating the roll-ups causing the second Project save procedure because the Project triggers didn't appear in my my debug logs a second time, before the error occurred.

Since my WE_IMProjCmpltnUp Trigger isn't updating the Projectobject, I was able to change it's execution to after insert, after update and this prevents the error from occurring.
This is because you can't update a record from within it's Before trigger, even though the roll-up actually causes the recalculation in the After context.
Again, it looks like this is Salesforce anticipating the change but not taking into account the context, when that change will be made.

Note - it wasn't necessary to also change the WE_ProjxOppUp Trigger to after update & doing that but not changing WE_IMProjCmpltnUp does not solve the issue.

Applying the solution from the Salesforce Knowledge Article on this topic - How to avoid Recursive trigger - which also seems to have been proposed in this answer didn't solve my issue.

Related Topic