2

I have a scenario where, on insert of Events (Activities), I need to insert records related to the Events, and then tie them back to the Event by populating a custom field on the Event object with the Id of the corresponding newly created record.

In before update context, I would do something like this:

// Build Map
Map<Id,CustomObject__c> eventIdToObjMap = new Map<Id,CustomObject__c>();
for(Event record : recordList){
  CustomObject__c obj = new CustomObject__c();
  eventIdToObjMap.put(record.Id,obj);
}

// Insert New Values
insert eventIdToObjMap.values();

// Relate Back to Event
for(Event record : recordList){
  CustomObject__c obj = eventIdToObjMap.get(record.Id);
  if(obj != null){
    record.CustomField__c = obj.Id;
  }  
}

However, in before insert context, we of course have no Id, which changes the approach.

I've considered using the sObject as the key, but, per the Apex Developer Guide under "sObject Map Considerations":

Be cautious when using sObjects as map keys. Key matching for sObjects is based on the comparison of all sObject field values. If one or more field values change after adding an sObject to the map, attempting to retrieve this sObject from the map returns null. This is because the modified sObject isn’t found in the map due to different field values. This can occur if you explicitly change a field on the sObject, or if the sObject fields are implicitly changed by the system; for example, after inserting an sObject, the sObject variable has the ID field autofilled. Attempting to fetch this Object from a map to which it was added before the insert operation won’t yield the map entry

So, my question is - If I am just using the sObject key in the context of Trigger.new, scoped specifically to before insert context, and I am only manipulating the data of that Key object after I no longer need to reference it, should I consider this a safe option? Or should I just stick to an Integer index = 0 approach where I index each record during the loop with index++ and use the Integer for the map key? I just want to make sure there aren't any "gotchas" that I'm not thinking of that could cause the sObject to change, invalidating the key, outside of my own code.

3
  • You can try this in after context as well. You will have recordId in both insert and update scenario and then you can add a check to handle recursion. Won't this approach work for you? Jun 4, 2021 at 19:35
  • From an execution performance perspective, it is best practice to do same-record updates in before context specifically so that we don't need to account for recursion and the cost of running our code twice in a large enterprise org. Jun 4, 2021 at 19:40
  • i'd use the Integer index
    – cropredy
    Jun 4, 2021 at 20:41

3 Answers 3

3

It's better to use a Map<Integer, sObject> in this case. You can then determine which record you're looking at.

// Build Map
Map<Integer,CustomObject__c> recordIndexToObj = new Map<Integer,CustomObject__c>();
Integer index = 0;
for(Event record : recordList){
  CustomObject__c obj = new CustomObject__c();
  recordIndexToObj.put(index++,obj);
}

// Insert New Values
insert recordIndexToObj.values();

// Relate Back to Event
index = 0;
for(Event record : recordList){
  CustomObject__c obj = recordIndexToObj.get(counter++);
  record.CustomField__c = obj.Id;
}

If you want to conditionally insert these records, you can use this design too, just use a for loop instead of a for-each loop:

// Build Map
Map<Integer,CustomObject__c> recordIndexToObj = new Map<Integer,CustomObject__c>();
for(Integer index = 0, size = recordList.size(); index < size; index++){
  Event record = recordList[index];
  if(conditionsAreMetFor(record)) {
    CustomObject__c obj = new CustomObject__c();
    recordIndexToObj.put(index,obj);
  }
}

// Insert New Values
insert recordIndexToObj.values();

// Relate Back to Event
for(Integer index: recordIndexToObj.keySet()) {
  recordList[index].CustomField__c = recordIndexToObj.get(index).Id;
}
3

You should avoid using SObject as map keys in almost all situations, and it would be much wiser here to just lean on the list order remaining static. Specifically, I would be worried about cases where you have two records whose every field has the same value.

Still worth considering this logic as an after insert, but it's an awkward fit either way. If you are really struggling with performance, you may need to asynchronously update the original event records from your custom object trigger.

2

What you're proposing sounds relatively safe to me, as long as you're:

  • inserting your related records in the before insert context.
  • not making any changes to the instance of the record in trigger.new from the time that you put it into the map until after you get the Id for your newly inserted related record

Something like

// Prepare a place to relate records to one another, in memory, without using Ids
Map<Event, MyObject__c> eventToRelatedRec = new Map<Event, MyObject__c>();

// Iterate over the events to generate the related records, and track which event they
//   belong to
for(Event evt :trigger.new){
    MyObject__c relObj = new MyObject__c(field = value, field2 = value);

    // No changes allowed to the evt instance from here...
    eventToRelatedRec.put(evt, relObj);
}

insert eventToRelatedRec.values();

for(Event evt :eventToRelatedRec.keySet()){
    // ...until after this line
    MyObject__c relObj = eventToRelatedRec.get(evt);

    evt.My_Rel_Obj__c = relObj.Id;
}

You could run into issues with indavertently trying to change a collection as you're iterating over it (which could throw an exception), and I believe that once you make a change to the key, the association between the key and the value is unrecoverable (though I'm fairly sure you can still at least get the values by calling the values() method. The code I tossed together to try to demonstrate this somehow retained the keys, the values, and the association between them, so shrugs)

Safer than that, however, would be to have a simple class to hold instances of both of your objects. It's not much more in the way of typing, and it's mostly a one-time cost.

private class EventRel{
    public Event evt;
    public MyObject__c related;
}

// Prepare a place to relate records to one another, in memory, without using Ids
List<EventRel> eventRelList = new List<EventRel>();
List<MyObject__c> relObjectsToInsert = new List<MyObject__c>();

// Iterate over the events to generate the related records, and track which event they
//   belong to
for(Event evt :trigger.new){
    MyObject__c relObj = new MyObject__c(field = value, field2 = value);

    EventRel relationTracker = new EventRel();
    relationTracker.event = evt;
    relationTracker.related = relObj;

    eventRelList.add(relationTracker);
    relObjectsToInsert.add(relObj);
}

insert relObjectsToInsert;

for(EventRel evtRelation :eventRelList){
    evtRelation.event.My_Rel_Obj__c = evtRelation.related.Id;
}

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.