4

I have a visualforce page where i get a serial number and making callout to get the related information from external system on click of a button.

Before making the callout i need to insert a record which holds the serial number so that when the callout returns the information, i can store it in same record.

But when doing this i get

CalloutException: You have uncommitted work pending

in controller method.

Am getting attachments from User to get stored in the inserted record as well. So i cannot do the insert after making callout since record id is required to use as parentid for attachment.

Visualforce page

<apex:inputText value="{!serialNumber}"/>
<apex:commandButton value="Add" action="{!getSerialNoDetails}"/>

Controller

public void getSerialNoDetails() {

SerialDetailsResponse response = SerialDetailsResponse();
Serial_Details__c serialDetails = new Serial_Details__c(Serial_Number__c = serialNumber);
insert serialDetails;
try {
    // Callout
    response = SerialDetails.getSerialDetails(serialDetails.Serial_Number__c);
    if(response != NULL) {
        serialDetails.Material_Number__c = response.MaterialNo;
        .
        .           
    }
}
catch(Exception e) {
    System.debug(e);
}

Any idea on how to avoid this exception? Thanks in advance.

  • 1
    Presumably SerialDetails.getSerialDetails is the method making the callout? How is it implemented? I suspect it isn't using @future, is it? – pchittum May 29 '15 at 11:03
  • I would try putting the callout into a @future method and call this from an after trigger on the Serial_Details__c object. – Rob May 29 '15 at 11:04
  • No its not. I want the result to be shown to User in VF page, get some attachments and update the Serial_Details__c object with details from callout. By using @Future, the results will not be returned in same transaction i read. – Vigneshwaran G May 29 '15 at 11:07
  • I modified my post to explain the reason i avoid future annotation @vamsiKrishna – Vigneshwaran G May 29 '15 at 12:43
13

Please check this link it may help: https://help.salesforce.com/apex/HTViewSolution?id=000003701

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

Description

Sometimes a developer needs to create a record and then update it with information provided by a Web Service. However, a Web Service Callout may not occur after a DML statement within the same transaction. To achieve the required action, the transaction must be separated into two parts so that the DML transaction is completed before the Web Service Callout occurs.

Resolution

This workaround splits the transaction into two separate Ajax processes. The first inserts the record and the second performs the callout and is able to update the newly inserted record.

TestWsCallout.page

<apex:page controller="TestWsCallout" tabstyle="Account">
    <apex:form >
        <apex:actionFunction action="{!InsertRecord}" name="InsertRecord_JS" Rerender="statuses" status="Status1" oncomplete="CallWebService_JS();"/>
        <apex:actionFunction action="{!CallWebService}" name="CallWebService_JS" status="Status2" reRender="statuses, msg"/>
        <apex:outputPanel id="statuses">
            <apex:actionStatus id="Status1" startText="...Inserting Record Into DB..." />
            <apex:actionStatus id="Status2" startText="...Calling Web Service..." />
        </apex:outputPanel>
        <apex:outputPanel id="msg">
            <apex:pageMessages />
        </apex:outputPanel>
        <div><input name="DoAction" class="btn" type="button" value="Do Action" onclick="InsertRecord_JS();return false;"/></div>
    </apex:form>
</apex:page>

TestWsCallout.cls

public class TestWsCallout{

    Account myAccount;

    public PageReference InsertRecord() {
        myAccount = new Account(name = 'Test Account');
        insert myAccount;
        // Calling a Web Service here would throw an exception
        return null;
    }

    public PageReference CallWebService() {

        // Execute a call to a Web Service
        HttpRequest req = new HttpRequest();
        req.setEndpoint('http://MyWebService12345678790.com?id=' + myAccount.Id);
        req.setMethod('GET');
        HttpResponse response = new Http().send(req);

        // Simulate an update
        myAccount.Name = 'Test Account 2';
        update myAccount;        
        ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.CONFIRM, 'WebService Called on New Account: ' + myAccount.Name));
        return null;
    }
}
  • 1
    Nice. As link only answers are discouraged here, could you add some more detail and then your answer is +1 from me. – Keith C May 29 '15 at 12:28
  • Wow. I couldn't find this link in my google page. May be i missed. Tried this and it works very well in UI. But when writing unit test method, the same error is thrown as we don't have any way to separate two transactions. Any ideas? – Vigneshwaran G May 29 '15 at 12:35
  • You can try to use setCurrentPage method in your class and then call the class methods: PageReference pageRef = Page.VFPageName; Test.setCurrentPage(pageRef); ClassName controller = new ClassName(); controller.mehtod1(); Hope this may help. – Nebula May 29 '15 at 13:05
5

By far the simplest approach is to do the insert after the web service callout in the response != NULL block; at first sight all that is being passed to the web service is the serialNumber. (If SerialDetails.getSerialDetails requires data to have been inserted before it is called refactor that too.)

If there really is a need to to the insert before the call, then one approach is to move the web service call into a separate request. That could be done for example by returning from getSerialNoDetails a PageReference that has redirect set to true so the client makes a new request for the same page and setting a parameters that indicates to do the web service request in that second page request (say in the apex:page action). Other parameters will also need to be carried across.

As far as the user is concerned, they have clicked a button once and they see the results in a page; to the platform its two requests, one to do the insert and another to make the web service call (and present the results of that call). Any solution based on asynchronous processing (such as @future) can't reliably get the results presented to the user.

4

I had a similar situation once. As per the documentation, Salesforce doesn't allow any DML operation before a callout in the same transaction. There seem to be two workarounds at the top of my head now.

Use @future annotation: - This is one way but I guess you don't want it.

The second way is using an internal callout to change the transaction before making the external callout. It is a bit tricky but it works well. The key here is to change the transaction before making the final callout.

Suppose, the developer clicks the button (let's name the transaction as T1) and then the developer expects some DML operation to happen.

Instead of using DML statements in the same class, create an another class (say handler class) as a Rest resource (with @RestResource annotation). This class should be able to deserialize the request into proper format and then insert the same. Please note this will happen in a different transaction T2.

This is making callout within salesforce.

class ABC_controller{

Http h = new Http();
Httprequest req = new Httprequest();
req.setBody(//pass the serialized JSON string of the objects to be inserted);
req.setEndpoint(//from urlmapping of the other handler class);
h.send(req);
}

Send() method will hit the appropriate endPoint and that handler class will deserialize the JSON string using JSON.deserialize() and insert the same.

After this, the transaction will return from T2 to T1 again and then you can make your external webservice callout normally.

3

There is a limitation in salesforce that you can't do a DML operation and callout in one single context. There are 2 alternatives I can think of now.

  1. Use @future method for callouts and update the Serial_Details__c record within the future method, you don't have to return anything back to the page. Now in order to show the returned data in the page, you can use Actionpoler component which will keep on refreshing your output component for data. I dont have the code handy, google about actionpoler.

  2. Call the webservice from insert trigger of Serial_Details__c, I am not sure but this may treat it as a different context and let you make the callout.

Not the answer you're looking for? Browse other questions tagged or ask your own question.