6

I have written inline Vf page to update the record value. The values are coming from a web service. While updating the record the detail page redirected to VF page. But the record was committed in the database with the error in debug log

"You have uncommitted work pending. Please commit or rollback before calling out"

I want to save the value in the record and the page remains in the same detail page. How could I achieve this?

controller:

public with sharing class  ServiceOptionsChange_CLS{ 

public string strId;
public String strOppid{get;set;}
public String strModelCode {get;set;}
public String strVersionCode {get;set;}
public List<SelectOption> lstOptions {get; private set;}
List<String> lstC2Goptions= new List<String>();
public String strop{get;set;}
public Service__c objService{get;set;}
public boolean refreshPage{get;set;}
public String strCode {get;set;}
public String strLabel {get;set;}
public Map<String,Double> mapOptionCodePrice = new Map<String,Double>();

public ServiceOptionsChange_CLS(ApexPages.StandardController controller) {
    objService = new Service__c();
    strId=ApexPages.currentPage().getParameters().get('id');
    Service__c objService=[select Opportunity_car_set__c from Service__c where id=:strId]; 
    strOppid=objService.Opportunity_car_set__c; 
    Opportunity_car_set__c opp=[SELECT Model__r.ProductCode,Version__r.version_code__c,Country_Code__c FROM Opportunity_car_set__c where id=:strOppid];    
    strModelCode    = opp.Model__r.ProductCode;
    strVersionCode  = opp.Version__r.version_code__c;
    system.debug('<<<<<selected value'+strModelCode+''+strVersionCode);

  } 


public List<SelectOption> getOptions(){

   OptionDetails_CLS optionValue=new OptionDetails_CLS();
    if(String.isNotBlank(strModelCode) && String.isNotBlank(strVersionCode)){
        lstC2Goptions=optionValue.getPriceListbasedOnReference(strModelCode,strVersionCode);
   }
    if(lstC2Goptions.size()>0){
        lstOptions = new List<SelectOption>();
        lstOptions.add(new SelectOption('','--Select--'));
        for(String strC2Goptions:lstC2Goptions) {
            lstOptions .add(new SelectOption(strC2Goptions,strC2Goptions));
        }
    }
  return lstOptions ;
}


public Pagereference customSave(){

    strop=objService.CodeC2G__c;
    String strQuantity=String.valueof(objService.Quantity__c);
    system.debug('<<<<<selected value in save'+strop+'quanity'+strQuantity);
    strCode=strop.substringbefore('-').trim();
    strLabel =strop.substringafter('-').trim();
    system.debug('<<<<<strCode'+strCode+''+strLabel);
    objService.id=strId;
    objService.Code__c=strCode;
    objService.Label__c=strLabel;
    Double dbPriceHT;
    OptionDetails_CLS optionValues =new OptionDetails_CLS();     
    mapOptionCodePrice = optionValues.getOptionPrice(strModelCode,strVersionCode);
    if (mapOptionCodePrice.containsKey(strCode)) {
        dbPriceHT = mapOptionCodePrice.get(strCode);
    }
    if (dbPriceHT!=null) {
        objService.Price_HT__c=dbPriceHT;
    }

    try{
        update objService;
        refreshPage=true;
        system.debug('<<<<<objService up'+objService);
        return null;
    }
    catch(Exception ex){
        System.debug('Error in SOQL Fetching'+ex);
        ApexPages.Message msg = new ApexPages.Message(Apexpages.Severity.ERROR, ex.getMessage() );
        ApexPages.addMessage(msg);
       return null;
        }
}

 }

vf:

<apex:page standardController="Service__c" extensions="ServiceOptionsChange_CLS" sidebar="true" >
<apex:form >
<apex:pageBlock >
<apex:pageblockbuttons location="bottom">
<apex:commandbutton value="Save" action="{!customSave}" /> 
</apex:pageblockbuttons> <apex:pageblocksection columns="2" title="Main Information" >
<apex:selectList size="1" value="{!objService.CodeC2G__c}" >
<apex:selectOptions value="{!Options }" > </apex:selectOptions>
<apex:actionSupport event="onchange" action="{!customSave}" reRender="a" />
</apex:selectList>
<br></br>
<apex:inputfield value="{!objService.Quantity__c}" required="false" />
<apex:pageblocksectionitem />
</apex:pageblocksection>
</apex:pageBlock>
</apex:form>
</apex:page>
  • That error is because you can't callout after DML, you need to do a callout first or put it in a @future method. It is not very clear where are you doing your callout here, you can refer to this post for more info on the error salesforce.stackexchange.com/questions/41440/… – Oles Malkov Mar 24 '16 at 8:35
6

The issue occurs when you first perform DML and then callout in the same transaction

check this doc

You have uncommitted work pending. Please commit or rollback before calling out

You need to perform Callout in the future method so your callout and DML will be in the different transaction

This way you can resolve this issue

create a list and serialize it to pass to the future method. Because we can't pas list of sobject as a parameter in the future method

insert listofAccount;

string jsonString = JSON.serializePretty(listofAccount );

callout(jsonString);

In future method make callout

@future(callout = true)
public static void callout(string jsonString){
    //do callout here rest or soap
}

this way you can solve this issue.

  • Actually, the documentation linked to in this answer explains that the "Web Service Callout may not occur after a DML statement". So the issue is caused by executing DML prior to the callout, not after. The author of the answer has it backwards. – SF1Dev Oct 25 '18 at 14:20
  • @SF1Dev thanks for bringing in my notice. Updated answer now – Ratan Paul Oct 26 '18 at 3:32
3

So many wrong answers here... You definitely CAN do a callout and then DML in same transaction. You cannot do it THE OTHER WAY AROUND. Only Oles Malkov got it right.

Also switching to @future may not always help, as Asnyc Apex also may result in the same exception:

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts_wsdl2apex_testing_dml.htm

  • Ratan Paul's answer has been updated to correct the order. – Zhaph - Ben Duguid Sep 18 at 9:55
0

The only way I was able to solve this was to move my upsert into it's own Apex class. This code would not work from a Controller I built to handle all my Rest API callouts.

Public class Helpers
{
    @future
    public static void SaveToken(string currentToken, DateTime expires)
    {
            //Save Token
            string p1 = currentToken.substring(0,255);
            string p2 = currentToken.substring(255,263);

            COMPANY_Settings__c setting = COMPANY_Settings__c.getOrgDefaults();
            setting.TokenP1__c = p1;
            setting.TokenP2__c = p2;
            setting.Expires__c = expires;
            upsert setting;
    }
}

I'm calling this method from my last callout response getStatusCode method like so. This is the only way I know that all my callouts have completed.

if (response2.getStatusCode() == 200) {
  Helpers.SaveToken(AppClaim.Token, AppClaim.Expires);
}

I'm new to Apex and Salesforce coding, so this was so frustrating to say the least. It makes no sense why this method would not work, as the instructions state, with the main Controller that I'm using. However, I like this method better, because I'm hoping that I can do all my database upserts, inserts etc... using this Helper class. Hope this helps someone else. No wonder there are so many questions on this littered all over the Salesforce forums.

I also had to split the Token into two strings (over 256), which was also frustrating. :)

  • 2
    It's not the separate class that makes the difference; it's the @future annotation, which pushes the DML into a separate transaction from the callout. See Ratan's answer for more details. – David Reed Aug 6 '18 at 15:45
  • The @future method did not matter. It would not work in the main Controller I built. Trust me on this. I refactored this over and over. As you can imagine, I need the Token to do all my API calls before the entire process finishes. This results in needless calls to get a Token the first time the app runs before I can finally save the Token and re-use it before it expires. This is ridiculously inefficient. Thanks for the comment. If you know of a better way to do this, please let me know. Ideally, I would like to save the initial callout to get the Token. I imagine it will fail, though. – Dumber_Texan2 Aug 6 '18 at 15:56
  • David's comment (and Ratan's answer, although there are some best practice concerns) is correct here. Salesforce does not allow DML (insert, update, upsert, delete, undelete) in a given transaction after a callout is performed, and the function of the @future annotated method is to ensure that your DML is in a separate transaction. That aside, it sounds to me like you have some material here for a couple of good questions. I'd encourage you to ask some questions about your code and drink from the well of knowledge that is SFSE. – Derek F Aug 6 '18 at 17:27
  • Derek, thanks for the comment. It will be interesting to see what happens when I batch this process seeing as I have to save the Token at the end if in fact a new Token was issued due to the old one expiring. – Dumber_Texan2 Aug 6 '18 at 20:34

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.