0

I am using a staging object in Salesforce to receive data from a middleware platform via SOAP API and I wish to then write a before (or after if need be) trigger which does the following:

  1. Splits the record into two records.
  2. Upserts both records.
  3. If either error then rollback the changes then addError() to the original record that was inserted from the middleware platform.

Does anyone have any examples or solutions to execute this in a trigger? I am struggling with both the rollback and the association of the error back to the original record (needs to be written to handle large amounts of data ie bulkify dml)

EDIT: The reason I need to split it into two records is that this record should be two different records, but the middleware team refuse to transform the data in their platform hence the need to do it in a staging object in Salesforce. To give some context; for each user there is a target and an actual amount being sent from the middleware. I need one record with target and one record with actual (the target object simply has User__c and Amount__c fields, whereas the staging object has User__c, Target__c and Actual__c

7
  • @AdrianLarson no worries have elaborated Mar 14 '16 at 23:33
  • so, when the middleware inserts the staging object, your trigger then creates 2 new objects of some different type? and if that fails, you want to fail the middleware insert so the error flows back to the client code? Can the middleware handle errors from sfdc dml operations (retry logic for example)
    – cropredy
    Mar 14 '16 at 23:42
  • @crop1645 creates two records in another object (if that is what you meant then yes). And for each two records that are created, if one or more fails then roll back all changes (the upserted 2 records) and add the error back to the original record). Hope that clarifies the flow... Mar 14 '16 at 23:43
  • but adding the error back to the original record means the middleware insert also fails ..right? That is, the SOAP API call and split up into two objects is one transaction?
    – cropredy
    Mar 14 '16 at 23:44
  • 1
    use an after insert trigger and a map correlating the index (integer) of the rows being upserted to the id of the staging object. then you can use trigger.newMap.get(stagingRecordId).addError(..)
    – cropredy
    Mar 14 '16 at 23:54
2

Assume the following

Staging__c // object inserted by SOAP API
Foo__c  // target object 1 built from Staging__c
Bar__c  // target object 2 built from Staging__c

Assume also that partial successes are allowed; if it is all-or-nothing, then code gets simpler

After insert trigger on Staging__c

Foo__c[] fooInsList = new List<Foo__c>();
Bar__c[] barInsList = new List<Bar__c>();
for (Staging__c s: Trigger.new) {
   fooInsList.add(new Foo__c(fld1 = s.fldX, ...);  // do the split part 1
   barInsList.add(new Bar__c(fldA = s.fldY, ...);  // do the split part 2
}

// Our correlated errors map; key is the 0...n index into Trigger.new
// relies on fact that each staging record generates 1 foo and 1 bar record
map<Integer,List<String>> stagingIxToErrorsMap = new map<Integer,List<String>> ();
for (Integer i = 0; i < Trigger.new.size(); i++)
    stagingIxToErrorsMap.put(i,new List<String>();

Database.SaveResult[] srList = Database.insert(fooInsList,false); // insert foos, partial success allowed
for (Integer i= 0; i < fooInsList.size(); i++)
    if (!srList[i].isSuccess()) // failed? save the error message
       stagingIxToErrorsMap.get(i).add(srList[i].getErrors()[0].getMessage());

srList = Database.insert(barInsList,false);  // insert bars
for (Integer i= 0; i < barInsList.size(); i++)
    if (!srList[i].isSuccess()) 
       stagingIxToErrorsMap.get(i).add(srList[i].getErrors()[0].getMessage());

// Report back to Soap client via addError any rows that had 1+ errors
for (Integer i = 0; i < Trigger.new.size(); i++)
   if (stagingIxToErrorsMap.get(i).size() > 0)
       Trigger.new[i].addError(String.join(stagingIxToErrorsMap.get(i),'\n'));

// At this point, if Foo[x] succeeded but Bar[x] failed, 
// the inserted Foo[x] needs to be rolled back; and vice-versa. 
// If both failed, no rollback required

Sobject[] rollbackSobjList = new List<Sobject>();
for (Integer i = 0; i < Trigger.new.size(); i++)
   if (stagingIxToErrorsMap.get(i).size() > 0) 
      rollbackObjList.addAll(new List<Sobject>{fooInsList[i],barInsList[i]});

Database.deleteResult[] drList = Database.delete(rollbackSobjList); 
for (Database.DeleteResult dr : drList) {
    // do something..you can ignore errors on records that don't exist
    // if delete fails on records that were inserted, then you could
    // fail the whole batch as this is indicative of a rollback failure
}

I typed this in so some typo-type compile errors may be present

There are other patterns to solve this and others may jump in with their favorites

2
  • This method works perfectly thank you but I have one issue - I am upserting the two records that are the result of the split, not inserting. So in your error handling at the tail of the class, is it possible to rollback the changes made in the upsert (ie if it was inserted it will remove the record, if it was updated it will rollback the update but only on that record)? I know how to achieve this using save points but I only want to pick out individual records, not the entire trigger. Mar 15 '16 at 22:27
  • I'll leave this as an exercise to the reader but YES, you'll need to hold onto a deep clone copy of the record before upsert so you can restore it via an update if the upsert fails
    – cropredy
    Mar 16 '16 at 0:41

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.