function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Steve Berley [Left Propeller]Steve Berley [Left Propeller] 

Preventing trigger recursion when updating related records on the same object

Let’s say you’re helping someone run the errands of daily life.  A trip may contain the following segments…each is its own record in an object that are all children of an umbrella trip object
  1. pick them up
  2. go to the doctor
  3. stop at the drug store
  4. get groceries
  5. return them home
Each segment includes driving directions between the specific addresses, so #3 includes the directions from the doctor’s office to the drug store and #4 starts at the drug store’s address.
 
Let’s say #3 changes because you have to go to a different drug store, then #4 will also need updating since its origin changed.
 
Typically, you’d update #3 using a before trigger.  No problem.  The trigger’s scope is naturally limited to the updated record (#3) and doesn’t include #4 which also needs updating since its origin changed.  As you’d expect, doing all of the updates in an after trigger causes trigger recursion.
 
Here’re the questions…
  1. How do I structure my trigger so it updates the changed record and related records without causing trigger recursion?
  2. Is it possible to expand the scope of the before trigger to include the related records?
For a bit of extra fun… Above includes mention driving directions.  Those are being populated by an @future call out to Google Maps, which also needs to update the same records without causing trigger recursion.
 
Note:  I've left out the code since issue is more about patterns and coding strategy.
 
Thanks for your help…
 
Matt Brown 71Matt Brown 71
You would need to check if the record that needs updating really needs to be updated.  Take the following example.
 
    // If Is After Update
    if (Trigger.isUpdate && Trigger.isAfter) {
        
        // Get Old Values
	    Map<Id, Individual> oldMap = Trigger.oldMap;
        
        // Iterate Individuals Updated
        for (Individual newIndividual: Trigger.new) {
            
        // If have previous individual data
	    if (oldMap.get(newIndividual.Id) != null) { 
                
                // Get previous info
		        Individual oldIndividual = oldMap.get(newIndividual.Id);
                
                // Was Marketing or Data Preference Changed?
                if (oldIndividual.HasOptedOutSolicit != newIndividual.HasOptedOutSolicit ||
                    oldIndividual.HasOptedOutProcessing != newIndividual.HasOptedOutProcessing ||
                    oldIndividual.PII_Restricted_Country__c != newIndividual.PII_Restricted_Country__c ||
                    oldIndividual.PII_Detected_Country__c != newIndividual.PII_Detected_Country__c
                ) {
                    // Do the needful
                    GDPRHandler.syncPreferencesToContact(newIndividual);
                }            
            }
        }
    }

So you would need to be thorough by checking if the field actually needs to be updated.  In my above example I check if the Marketing Preference was changed.
Then in the handler before I update the actual data:
 
// Truncated for brevity

Boolean newProccessValue = !contact.PII_Data_Processing__c;
Boolean newMarketValue = !contact.PII_Marketing_Contact__c;            
Boolean newOptedOutValue = !contact.PII_Marketing_Contact__c;
Boolean newRestrictedValue = contact.PII_Restricted_Country__c;
String newDetectedCountryValue = contact.PII_Detected_Country__c;

// Only Update If Needed (Prevent Recursion)
if (individual.HasOptedOutProcessing != newProccessValue ||
    individual.HasOptedOutSolicit != newMarketValue || 
    individual.PII_Restricted_Country__c != newRestrictedValue ||
    individual.PII_Detected_Country__c != newDetectedCountryValue
) {
    individual.HasOptedOutProcessing = newProccessValue;
    individual.HasOptedOutSolicit = newMarketValue;
    individual.PII_Restricted_Country__c = newRestrictedValue;
    individual.PII_Detected_Country__c = newDetectedCountryValue;
    update individual;
}

Hopefully this makes sense, and you're able to apply it to your situation.  If you want to post code we could give you more specific solutions.
Alain CabonAlain Cabon
Hi Steve,

I have used this trick (and that seems to work): Avoid Recursive Trigger Calls​
https://help.salesforce.com/articleView?id=000133752&language=en_US&type=1
 
public class checkRecursive {
     public static Set<Id> SetOfIDs = new Set<Id>();
}
 
trigger UpdateSegment on Segment__c (after update) {
    if (Trigger.isUpdate && Trigger.isAfter) {
        Map<Id,Decimal> tripIds = new Map<Id,Decimal>();
        // get all the trips and save the order of the changed segment and use the checkRecursive.SetOfIDs (Salesforce article, help)
        for (Segment__c newSegment: Trigger.new) {
            If(!checkRecursive.SetOfIDs.contains(newSegment.Id)){
                checkRecursive.SetOfIDs.add(newSegment.Id);
                tripIds.put(newSegment.Trip__c,newSegment.Order__c);
            }
        }
        if (tripIds.size() > 0) {
            // get all the segments of all the trips
            List<Segment__c> segs = [select id,trip__c,order__c,origin__c,destination__c from segment__c where trip__c in :tripIds.keySet()];
            Map<Id,Map<Decimal,String[]>> trips = new Map<Id,Map<Decimal,String[]>>();
            for(Segment__c seg:segs) {
                if (!trips.containsKey(seg.trip__c)) {
                    trips.put(seg.trip__c,new Map<Decimal,String[]>{seg.order__c=>new String[]{seg.id,seg.origin__c,seg.destination__c}}  ); 
                } else {
                    Map<Decimal,String[]> msegs = trips.get(seg.trip__c);
                    msegs.put(seg.order__c,new String[]{seg.id,seg.origin__c,seg.destination__c});
                }
            }
            // update the destination of the previous segments and the origins of the next segments
            List<Segment__c> updatsegs = new List<Segment__c>();
            for(Id tripId:trips.keySet()) {
                Map<Decimal,String[]> segm = trips.get(tripId);
                Decimal updatedseg = tripIds.get(tripId);
                String[] selected =  segm.get(updatedseg);
                String updated_origin = selected[1];
                String updated_destination = selected[2];
                Decimal prev = updatedseg - 1;
                Decimal post = updatedseg + 1;
                if (prev > 0) {
                    String[] fields = segm.get(prev);
                    updatsegs.add(new Segment__c(id=fields[0],destination__c=updated_origin));
                }
                if (post <= segm.size()) {
                    String[] fields = segm.get(post);
                    updatsegs.add(new Segment__c(id=fields[0],origin__c=updated_destination));
                }              
            }
            if (updatsegs.size()> 0) {
                update updatsegs;
            }            
        }
    }
}


User-added image


User-added image

The problem is when you modify many segments for the same trip with a batch process (not tested) but the static function checkRecursive works for individual change from the screen (I have got the recursive depth error without it).

 
Steve Berley [Left Propeller]Steve Berley [Left Propeller]
Great thanks to you both for the expert advice.  I ended up relying heavily on all of your suggestions to solve the challenge.
 
Here’s what I did.
 
I had been setting/clearing status bits but saving them to the segment records was exacerbating the problem.  Using Matt’s advice I’m now assessing status dynamically so I’m preventing trips through the trigger.
 
Using Alain’s checkRecursive trick I’m able to both control the number of trips through the trigger for the updated record and expand the scope so the related records also avoid trigger recursion; without a trigger execution counter.
 
Thank you!
 
Steve Berley [Left Propeller]Steve Berley [Left Propeller]
@Alain Asked privately that I post my code as this is a problem that while deceptively simple is actually quite hard to solve elegantly.  Because of the gentlemen here I've got an approcach to offer others...

Trigger:
trigger SegmentTrigger on Segment__c (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
	private static boolean didStandardSave = false;

	if (trigger.isAfter){
		// skip normal processing for already proecessed segments
		list<segment__c> toProcess = new list<segment__c>();
		if (trigger.isDelete){
			for (segment__c was : trigger.old){
				if ( !SegmentTriggerHandler.processedsegments.contains(was.id)) toProcess.add(was);
			}
		} else  {
			for (segment__c is : trigger.new){
				if ( !SegmentTriggerHandler.processedsegments.contains(is.id)) toProcess.add(is);
			} 
		}
		SegmentTriggerHandler.processSegments(toProcess);
	}
}

Handler:
Note:  This code was slimmed for presentation so please don't take it as fully functional....
 
global with sharing class SegmentTriggerHandler {
    // public SegmentTriggerHandler() { }
    
    private static map<id, trip> trips = new map<id, trip>(); //map <parent id, trip>

    // the following is used to allow the expansion of the scope of records to updated while
    // preventing them from being processed when saved by falling through the After Trigger
    // and in doing so preventing recursion
    public static set<id> processedSegments = new set<id>();

    public static string query = 'select the needed fields from segment__c';

    public class trip {
        public id parentID {get;set;}
        public id contactID {get;set;}
        public map<integer, segment__c> segments {get;set;}
        trip(){
            parentID = null;
            contactID = null;
            segments = new map<integer, segment__c>(); // map<seq#, segment__c>
        }
    }

    public static void processSegments(list<segment__c> segments){
        if (segments == null || segments.size() == 0) return;
        trips = loadTrips(segments);
        for (trip t : trips.values()) {
            trips.put(t.parentID, reflowTrip(t));
        }
        saveTrips(trips);
        updateMapping();
    }

    public static trip reflowTrip(trip t){
        t = cleanOrder(t);
        t = updateContinues(t);
        return t;
    }

    public static trip cleanOrder(trip t){
        // smoothes and rationalizes the sequence of the segment numbers
        // so 1, 3, 4, 7 becomes 1, 2, 3, 4
        return t;
    }

    public static trip updateContinues(trip t){
        // since segments tend to start at the endpoint of the previous one, this method ensures
        // continuity after segments have been deleted, inserted, or the order has changed
        return t;
    }

    public static void saveTrips(map<id, trip> trips){
        if (trips == null || trips.size() == 0) return;
        list<segment__c> segs = new list<segment__c>();
        for (trip t : trips.values()){
            if (t.segments != null && t.segments.size() > 0) {
                for (segment__c s : t.segments.values()){
                    segs.add(s);

                    // the following enables all of the related segments to pass 
                    // through the trigger without being processed again
                    processedSegments.add(s.id);
                }
            }
        }
        update segs;
    }

    public static map<id, trip> loadTrips(list<segment__c> segments){
        // loads all related segments in the trips associated with those passed in from the trigger
        // returns map<parent id, trip>
        trips = new map<id, trip>();
        return trips;
    }

    @Future(callout=true)
    public static void updateMapping(){
        list<segment__c> segments = [select id, segment__c, From_composite__c, To_composite__c,
            from_name__c,to_name__c, Directions__c from segment__c
            where From_composite__c != null and To_composite__c != null];
        list<segment__c> toUpdate = new list<segment__c>();
        for (segment__c s : segments){

            // Directions is being used as a status flag. if they need to be updated the 
            // field is nulled out.
            // Below ensures only the segments needing directions are sent to Google.
            if ( !string.isblank(s.directions__c)) continue;
        googlemaps.turnByTurn dir = googlemaps.turnByTurn(s.from_composite__c, s.to_composite__c);
            segment__c u = new segment__c();
            u.id = s.id;
            u.Directions__c = dir.directions;
            toUpdate.add(u);
        }
        update toUpdate;
    }
}
Alain CabonAlain Cabon
Thanks Steve.  This problem is more complicated than suspected at first glance and you have developed a great solution that might interest the community (including me). A posted working solution is always better than explanations and other people could give you further guidance if your solution can be further improved.