You need to sign in to do that
Don't have an account?

Internal Salesforce Error on sObject list insert
Hey all,
I posted a bit about this on twitter but I figured I'd make a thread to get some more details. What's happening is that I have a method (it's an Apex Rest method if that matters) whose job is to insert a list of sObjects. For some reason, attempting to do so causes an uncatchable Internal Salesforce Error. Weird part is, if I remove the offending code, and run it by itself, it works just fine. The object in question here is a custom object called survey_answer__c. These are created by deserializing some JSON from a remote webservice. For example
[{"Answer__c":2.0,"Id__c":"51345X1607X53053","Question__c":"what is blah 1?"},{"Answer__c":3.0,"Id__c":"51345X1607X53100","Question__c":"what is blah 2?"}]
Contains data for two answer objects to get inserted. So I deserailize it, which goes just fine. Now I have a list of sObjects. I attempt to insert it using the plain old insert command and I get my error. Now the weird part is if I mimic this setup by itself in an execute anonymous block using the following code, it will run fine. You'll notice all the data for the objects is exactly the same, except for the second set has an Id for the survey_entry__c relationship field (which is appended to each sObject in the first example later on by iterating the list of sObjects and setting the field manually).
list<Survey_Answer__c> answers = new list<Survey_Answer__c>(); Survey_Answer__c ans1 = new Survey_Answer__c(); ans1.Question__c= 'what is blah 1?'; ans1.Id__c='51345X1607X53053'; ans1.Survey_Entry__c='a0zS0000001HSavIAG'; ans1.Answer__c='2.0'; Survey_Answer__c ans2 = new Survey_Answer__c(); ans2.Question__c= 'what is blah 2?'; ans2.Id__c='51345X1607X53100'; ans2.Survey_Entry__c='a0zS0000001HSavIAG'; ans2.Answer__c='3.0'; answers.add(ans1); answers.add(ans2); insert answers;
So there you have it. Random error for no good reason. I'll post the entire class below for you to review. If you want the object definiton files to play with it yourself, let me know and I can host them somewhere.
@RestResource(urlMapping='/importSurvey/*') global class importSurveyData { public static boolean isApexTest = false; //this method takes a post request with a survey id, a user token and other paramters about the survey. It reaches out to another //webservice which gets the survey answer data and returns it. The data is used to generate survey_answer__c objects and insert them. //It will also create a survey__c and survey_entry__c object if required (if they don't exist). Each campaign should have only one survey__c object //which can contain multiple survey entries (one per person who took the survey). Each survey entry can contain multiple answers. Each contact will //have only one survey entry for a survey /* Schema Campaign | +-> Survey__c | +-> Survey_Entry__c <- Contact | +->Survey_Answer__c */ @HttpPost global static list<Survey_Answer__c> doPost(RestRequest req, RestResponse res) { list<Survey_Answer__c> answers; String jsonString; Survey_Entry__c surveyEntry = new Survey_Entry__c(); //Get the ID of the survey we are working with from the URL string survey = req.params.get('survey'); //Loop over all the parameters passed in and try to assign them to the survey object. Just discard any unusable data for(string param : req.params.keySet()) { try { surveyEntry.put(param,req.params.get(param)); } catch(Exception e) { } } //We have to call out to a web service to get the lime survey answer data. It will get returned as a JSON encoded array //of answer objects. These can be directly de-serialized to survey_answer__c objects. They contain all the actual answers //to the questions as well as data about the questions itself try { HttpRequest getDataReq = new HttpRequest(); getDataReq.setMethod('GET'); getDataReq.setEndpoint( 'http://xxxxxxxxxxxxxxxxxxxxxxxx.com/?method=getLimeSurveyAnswers&surveyId='+survey+'&Token='+surveyEntry.token__c); Http http = new Http(); if(!isApexTest) { //Execute web service call here HTTPResponse getDataRes = http.send(getDataReq); if(getDataRes.getStatusCode() == 200) { jsonString = getDataRes.getBody(); } else { system.debug('Could not contact web service'); } } else { system.debug('Apex Test Mode. Webservice not Invoked'); jsonString = '[{"Answer__c":2.0,"Id__c":"51345X1607X53053","Question__c":"what is blah 1?"},{"Answer__c":3.0,"Id__c":"51345X1607X53100","Question__c":"what is blah 2?"}]'; } //This is just put here for testing. It's faster and more reliable for testing to just have the same JSON content every time //in production this line would be removed jsonString = '[{"Answer__c":2.0,"Id__c":"51345X1607X53053","Question__c":"what is blah 1?"},{"Answer__c":3.0,"Id__c":"51345X1607X53100","Question__c":"what is blah 2?"}]'; //deserialize the list of answers retreived from the webservice call answers = (List<Survey_Answer__c>) JSON.deserialize(jsonString, List<Survey_Answer__c>.class); //Now we need to find the ID of the survey to attach these answers to. This method will either find the existing //survey_entry__c id (based on token, contact, and lime survey id) or create a new one and return it. Either way //it's going to return a survey_entry__c object (it will also create a survey object to attach itself to if there isn't one) Id surveyEntryId = importSurveyData.getSurveyEntry(surveyEntry, survey).id; //We don't want duplicate data, and this may get run more than once, so delete any existing answer data for this survey entry List<Survey_Answer__c> toDelete = [select id from Survey_Answer__c where Survey_Entry__c = :surveyEntryId]; delete toDelete; //We now also need to update these survey answer objects with the survey_entry__c id so the relationship gets set for(Survey_Answer__c ans : answers) { ans.Survey_Entry__c = surveyEntryId; } insert answers; //This line errors for no discernable reason. } catch(Exception e) { system.debug('======================== ERROR IMPORTING ANSWERS: ' +e.getMessage() + ' ' +e.getCause() + ' ' +e.getTypeName()); } return answers; } //this will either find an existing survey_entry__c or create a new one if one does not exist. //it will also request creation of a survey__c object to attach itself to if one does not exist. global static Survey_Entry__c getSurveyEntry(Survey_Entry__c surveyEntry, string surveyId) { //try to find existing survey entry for this person list<Survey_Entry__c> entries = [select id, Survey__c, token__c, contact__c from Survey_Entry__c where survey__r.study__r.lime_survey_id__c = :surveyId and contact__c = :surveyEntry.contact__c limit 1]; //if a survey entry is not found, create one otherwise return existing if(entries.isEmpty()) { surveyEntry.Survey__c = getSurvey(surveyId).id; insert surveyEntry; return surveyEntry; } else { return entries[0]; } } //create a survey for the given campaign if does not exist, otherwise return the existing one. global static Survey__c getSurvey(string surveyId) { //find the existing survey if there is one for this id list<Survey__c> surveyObj = [select id from Survey__c where Study__r.Lime_Survey_ID__c = :surveyId limit 1]; if(surveyObj.isEmpty()) { Survey__c survey = new Survey__c(); RecordType ParentRecordType = [select id from RecordType where name = 'FPI Parent Campaign' ]; list<Campaign> parentCampaign = [select id from Campaign where Lime_Survey_ID__c = :surveyId and recordTypeId = :ParentRecordType.id order by createdDate asc limit 1]; survey.Study__c = parentCampaign[0].id; insert survey; return survey; } else { return surveyObj[0]; } } @isTest public static void importSurveyDataTest() { isApexTest = true; string surveyID = '51345'; //Insert the check record, with testContact as the contact Account thisTestAccount = testDataGenerator.createTestAccount(); Contact thisTestContact = testDataGenerator.createTestContact(thisTestAccount.id); Campaign thisTestUmbrellaCampaign = testDataGenerator.createTestUmbrellaCampaign(thisTestContact.id); Campaign thisTestParentCampaign = testDataGenerator.createTestParentCampaign(thisTestUmbrellaCampaign.id, thisTestContact.id); thisTestParentCampaign.Lime_Survey_ID__c = surveyID; update thisTestParentCampaign; RestRequest req = new RestRequest(); RestResponse res = new RestResponse(); //Do a sample request that should invoke most of the methods req.requestURI = 'https://cs1.salesforce.com/services/apexrest/importSurvey'; req.httpMethod = 'POST'; req.addParameter('survey',surveyID); req.addParameter('token__c','mkwcgvixvxskchn'); req.addParameter('contact__c',thisTestContact.id); req.addParameter('type__c','survey'); req.addParameter('label__c','test survey'); req.addParameter('result__c','qualified'); list<Survey_Answer__c> importedAnswers = doPost(req,res); system.debug(importedAnswers); //The first thing it should have done is create the survey object itself, attached to the master campaign. list<Survey__c> testSurvey = [select id from Survey__c where study__r.Lime_Survey_ID__c = :surveyID]; system.assertEquals(1,testSurvey.size()); //now it also should have created a survey entry for this person, with the attributes passed in the request list<Survey_Entry__c> testEntry = [select id,type__c,label__c,result__c,token__c from Survey_Entry__c where contact__c = :thisTestContact.id and survey__c = :testSurvey[0].id]; system.assertEquals('survey',testEntry[0].type__c); system.assertEquals('test survey',testEntry[0].label__c); system.assertEquals('qualified',testEntry[0].result__c); system.assertEquals('mkwcgvixvxskchn',testEntry[0].token__c); //Also all the survey answers should have been imported. There should be two of them. list<Survey_Answer__c> answers = [select id, Answer__c, Question__c from Survey_Answer__c where Survey_Entry__c = :testEntry[0].id]; system.assertEquals(2,answers.size()); } }
Also, this is the code block I've been using to test the method while I've been attempting to debug this.
Also, the getSurvey and getSurvey entry method have been tested been themselves and operate as expected. They are able to create the survey and survey entry, but errors on the answer insertion. Not sure if it's expected behavior or not, but it does seem to roll back the database changes when the error happens. Meaning it does create the survey and survey entry objects during execution but as soon as it errors, they are removed/deleted.
One more bit of info. Doing a system.debug on the answers list right before insert prints off the object data as follows.
13:36:00:100 USER_DEBUG [99]|DEBUG|(
Survey_Answer__c:{Question__c=what is blah 1?, Id__c=51345X1607X53053, Survey_Entry__c=a0zS0000001HSavIAG, Answer__c=2.0},
Survey_Answer__c:{Question__c=what is blah 2?, Id__c=51345X1607X53100, Survey_Entry__c=a0zS0000001HSavIAG, Answer__c=3.0})

And here are a few screenshots
(http://i.imgur.com/5mRkK.png)
(http://i.imgur.com/qNNnA.png)
One more post. Here is the full stack trace as well.
http://pastebin.com/raw.php?i=nsALrCZx
I logged a bug with the apex team for this one, its not happy with the state of your SObjects, but no idea why.
Thanks for the bug, Simon. It's a bug in the new runtime, when doing DML on a list constructed by json.deserialize().
While we get this fixed, you can use the following workaround:
list<survey_answer__c> answers = new list<survey_answer__c>();
answers.addAll((List<Survey_Answer__c>) JSON.deserialize(jsonString, List<Survey_Answer__c>.class));
This workaround seemed to work for me. Thank you very much for that. I can finally get this code out in production! Always a good feeling :)
What would be the equivilent for deserializing to a single custom class, called limeSurveyWebServiceObject. I think I am having what equats to basically the same bug in another class while deserializing some JSON into a custom class type.
This particular bug is only applicable to lists. I'd have to see a failing piece of sample code to diagnose your other bug.
Ah, I was probably mistaken in my guess then. It's that classic 'Don't know type of object to deserialize' error. No workaround I've tried has worked for that one yet.
Is it happening consistently or sporadically? Which instance are you on?