0

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

  • By Tuple , do you mean to have a another map? – Mahmood Sep 26 '17 at 19:01
  • It does seem like the best way to handle this would be to store the related objects in a Map<SObject2, SObject1>. Then do a Database.insert() on new List<SObject2>(Map.keySet()). This way you can iterate through the results and get SObject1 to add an error to. – Jeff Bennett Sep 26 '17 at 19:05
  • @Mahmood, any data structure that would help me retrieve the values based off index. – Sagar Sep 26 '17 at 19:08
  • @JeffBennett, thanks for the quick suggestion. I can get started with this and can tweak whenever necessary – Sagar Sep 26 '17 at 19:11
0

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.

| improve this answer | |
1

There are two typical patterns which allow you to map data back to the source records when updating related records. Either way, you will want to use Id as your map key, not SObject.

Updating child records from parents

try
{
    update children;
}
catch (DmlException dmx)
{
    Map<Id, Parent__c> parentMap = new Map<Id, Parent__c>(parents);
    for (Integer i = 0; i < dmx.getNumDml(); i++)
    {
        Child__c child = children[dmx.getDmlIndex(i)];
        parentMap.get(child.Parent__c).addError(dmx);
    }
}

Updating parent records from children

try
{
    update parents;
}
catch (DmlException dmx)
{
    Map<Id, List<Child__c>> parentToChildren = new Map<Id, List<Child__c>>();
    for (Child__c child : children)
    {
        if (!parentToChildren.containsKey(child.Parent__c))
            parentToChildren.put(child.Parent__c, new List<Child__c>());
        parentToChildren.get(child.Parent__c).add(child);
    }
    for (Integer i = 0; i < dmx.getNumDml(); i++)
    {
        Parent__c parent = parents[dmx.getDmlIndex(i)];
        for (Child__c child : parentToChildren.get(parent.Id))
        {
            child.addError(dmx);
        }
    }
}
| improve this answer | |
  • Thanks Adrian for a detailed explanation. Sorry, I might have missed some information in the above context. I am working on the before insert triggers where I do not have the Id for any of the records, that's why I am looking to maintain an index which might help me figure of the erred record. Let me know, if there's anything that I am missing – Sagar Sep 26 '17 at 20:06
  • @Sagar If you are manipulating related records in a before insert context then they cannot be child records, but must be parents. Look more closely at the first example. It does not require the records you work with to already have an Id value. – Adrian Larson Sep 26 '17 at 20:13
  • I am working on before insert trigger on a parent. On a before insert trigger for SObject1, I am manipulating data which creates a record for SObject2. So, SObject1 and SObject2 have 1:1 mapping. I collect all SObject2`s to be inserted and insert List<Sobject2>. If I encounter DML exception while I try to insert the List<SObject2>, the getNumDML methods will help me identify which specific instance of SObject2 failed. Now, where I am struggling is to find the associated SObject1 and update its status to Error. Since its before insert, I do not have id of Sobject1. Hope, this helps – Sagar Oct 3 '17 at 19:14
0

How I traditionally handle this is to make two lists; the first holds the record which I will error upon, the second the records to insert/update/delete. This works based on the concept that the lists will remain in order.

SObject[] source = new SObject[0],
    updates = new SObject[0];
for(SObject record: Trigger.new) {
    if(<some condition>) {
        source.add(record);
        updates.add(new ...);
    }
}
Database.SaveResult[] results = Database.update(updates, false);
for(Integer index = 0, size = results.size(); index < size; index++) {
    if(!results[index].isSuccess()) {
        source.addError(results[index].getErrors()[0].getMessage());
    }
}

This is an easy, reliable way to match the results together, and allows you to deal with both parents and children (because the relationship is one-to-one between the two lists). It's not the only way, but it is simple.

| improve this answer | |

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.