[SalesForce] Combination of sObjects (Tuples) as Map Values

Is there a way in apex to create a Map<Integer, Tuple<SObject1, SObject2>> where the Integer is an index and the Tuple will hold the combination of SObjects. The process that I am trying to accomplish is where a trigger on SObject1 will try to create a record of type SObject2 and if SObject2 creation fails (DML exception), I want to be able to update the SObject1 status to error.

How: If the above map is possible, I can get the index of the Sobject2 record which failed and will be able to update SObject1 record at that index with the error status.

Is there a better way to do it? I am not sure if Tuples are allowed on the Salesforce platform. Any help is appreciated

Best Answer

Apex does not have the concept of a tuple, at least not natively.

You could try to create a wrapper class that holds both SObjects, or use a Map<Integer, List<SObject>>. Both of those approaches end up running into the same issue though; given an instance of SObject2, you wouldn't be able to (efficiently) find the corresponding SObject1 instance. I mean, linear time isn't the worst we could do, but we can find that correlation in constant time.

You could tie them together using a Map<SObject2, SObject1> as mentioned in the comments on your question, but using SObjects as keys in a map is a dangerous game (if any field is changed by any amount, you'll lose the mapping, and I don't think you could get it back by undoing the change).

Instead, I'd recommend simply using 2 separate lists. The first list holds your SObject1 records. You'd iterate over that list to generate your SObject2 records.

The key here is that Lists are ordered collections. When you iterate over your first List, you start at index 0. When you add SObject2 to the second list, it too will be added starting at index 0. Assuming that you create only and exactly 1 SObject2 record per SObject1 record, your two lists will be in lock-step with one another without the need for keeping an explicit index.

Adrian, in the catch block of his example code, uses two methods from the DML Exception class getNumDml() and getDmlIndex(). Documentation on those can be found at the bottom of the documentation on built-in exception classes.

getNumDml() tells you how many failures you had, and getDmlIndex() tells you which index in the list you performed DML on was the cause of the exception.

Putting everything together, we get something like

// Trigger.new provides one of our required lists for us, so we only need to create
//   a list for your SObject2 records.
List<SObject2> sobj2List = new List<SObject2>();

for(SObject1 record :Trigger.new){
    // By virtue of iterating over a List, and creating a corresponding list, the two
    //   lists are automatically correlated by the inherent list index.
    sobj2List.add(new SObject2(
        // set fieldName = value pairs here, each name = value pair separated
        //   by a comma
    ));
}

try{
    insert sobj2List;
} catch (DMLException e){
    for(Integer i = 0; i < e.getNumDml()){
        // We can use getDmlIndex with Trigger.new, and .addError to the corresponding
        //   record.

        Trigger.new[e.getDmlIndex(i)].addError('Inserting corresponding SObject2 record failed');
    }
}

This approach doesn't assume that your two SObjects have any lasting relationship between them. If your two SObjects have some relationship (Master-Detail, Lookup, other...), then Adrian's approach may be better.