[SalesForce] Handling Duplicate Id in List Exception

I have junction object ProjectxOpp__c which links Projects (MPM4_BASE__Milestone1_Project__c) with Opportunities. It's a many to many relationship so the same Opportunity can be linked to multiple Projects.

When I update my Project record's MPM4_BASE__Deadline__c field, I also want to update Opportunities which are linked to that Project by an ProjectxOpp__c junction record.
If I update a batch of Projects & as a result, update a batch of related Opportunities, it's possible that I end up trying to update the same Opportunity twice. This causes a System.ListException

Duplicate id in list

I've seen several solutions on the site for dealing with this issue but they either suggest rewriting an earlier section of the code, to avoid picking up the same record twice or using a Map to store the Ids & I can't see how to apply the latter solution in my code.

Neither explain how to handle duplicates that you're expecting..

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

    Map<String, Schema.RecordTypeInfo> RT = MPM4_BASE__Milestone1_Project__c.SObjectType.getDescribe().getRecordTypeInfosByName();
    List<VRTN__c> weRTs = VRTN__c.getall().values();
    List<String> weRTNames = new List<String>();
    Set<Id> validRecordTypeIds = new Set<Id>();

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

    for(VRTN__c weRT : weRTs) {
        try {
            weRTNames.add(weRT.NAEUProjs__c);
        } catch (System.StringException e) {
            System.debug(System.LoggingLevel.ERROR,'Invalid Record Type Name ' + weRT.NAEUOpps__c);
        }
    }

    for(String weRTN : weRTNames) {
        if(RT.get(weRTN) != null){
            validRecordTypeIds.add(RT.get(weRTN).getRecordTypeId());
        }
    }

    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;
    }

}

Best Answer

In your case you can have multiple MPM4_BASE__Milestone1_Project__c objects related to an Opportunity and your aim is to record the latest MPM4_BASE__Deadline__c date from any of them.

Here is one way to do that:

Map<Id, Opportunity> updates = new Map<Id, Opportunity>();
Map<Id, Opportunity> m = new Map<Id, Opportunity>();
for(ProjectxOpp__c j : [
        SELECT Project__c, Opportunity__r.Id, Opportunity__r.Implementation_Revenue__c
        FROM ProjectxOpp__c
        WHERE Project__c In :projects
        AND Opportunity__c != null
        ]) {
    Id oppId = j.Opportunity__r.Id
    Opportunity opp = m.get(oppId);
    if (opp == null) {
        opp = j.Opportunity__r;
        m.put(oppId, opp);
    }
    Date newCompDate = deadlineDates.get(j.Project__c);
    if (newCompDate > opp.Implementation_Revenue__c){
        opp.Implementation_Revenue__c = newCompDate;
        updates.put(oppId, opp);
    }
}
update updates.values();

The aim is to update each Opportunity once only: that can be done by using a map (called m here) keyed by the ID so there is only ever one instance of Opportunity kept even though many duplicates may be returned by the query. The map can be populated from your existing query: if there isn't an entry in the map it is added.

I've also added a second map (called updates) because only some of the Opportunity objects will need to updated.