2

I have a batch job with http callout enabled, in the execute() method, the code logic goes like:

global void execute(Database.BatchableContext BC, List<Object> scope){
   List<CustomObject> customObjects; //here build a list of custom object;
   insert customObjects;
   for(CustomObject customObject : customObjects){
      Id customObjectId = customObject.Id;
      //here make a http call out with the object id information;
   }
}

Debug log will give me error:

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
11:31:12.296 (1380679453)|FATAL_ERROR|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

I have read this article, but my context is a batch. Not sure how to split so that the custom object list is still available for http callout after splitting.

https://help.salesforce.com/articleView?id=000003701&type=1

8
  • You can do cal lout in your finish method
    – MIX DML
    Jan 24, 2019 at 9:56
  • 2
    You can use Database.Stateful method to get data transfer from execute to finish method.
    – MIX DML
    Jan 24, 2019 at 10:03
  • @MIXDML thanks for the guidance, and the solution works in final method. The remaining challenge is that only 100 http callout is allowed in one transaction (in the final method). Can we somehow extend it?
    – Xi Xiao
    Jan 24, 2019 at 11:38
  • One way to deal with this challenge is to chaining classes and make callout 100 and next 100 in next class.
    – MIX DML
    Jan 24, 2019 at 11:53
  • 1
    You could potentially chain into a separate batch class to make the callouts, if more than 100 is needed.
    – David Reed
    Jan 24, 2019 at 12:40

3 Answers 3

3

Batch chaining (bottom of the page here) is probably the most idiomatic solution, because it'll allow you to process more than 100 records and because your situation does not appear to be amenable to reordering operations to avoid the CalloutException.

The way this would probably need to go is that you'd implement two batch classes - say RecordCreationBatch and RecordCalloutBatch. RecordCreationBatch would chain into RecordCalloutBatch in its finish() method, and RecordCalloutBatch would make the actual callouts in its execute() method.

That leaves out one critical question - how the first batch communicates to the second which records it needs to process. That'll be dependent more on the details of your operation; you certainly could track a State__c on the new custom objects so RecordCalloutBatch knows which ones to process, execute similar queries in both batches, or potentially even pass a collection of the Ids (dependent on volume!).

1

To summarise the solution proposed by @MIXDML, here is how it worked out for me.

Add Database.stateful in the batch class so that the state information can be retained across batch execution. In my case, I need to keep a list of inserted object records.

Then, in the final method do individual HTTP Callout for each record. The end-point is a REST API, so I need to make callout one record at a time.

In this case, the governor limit of 100 callout in one execution is something I need to monitor. OR, I can create another batch job to run in the final method! This is explained well in David Reed's answer below.

1

I was facing a similar issue and created a little framework that served me well on various projects. See Github: salesforce-apex-retry.

Although not my intended use-case you could simply achieve the desired behavior with the added benefit of automatic rescheduling of the call-outs in case of failure. With these classes and custom object installed you could easily do the following:

    global void execute(Database.BatchableContext BC, List<Object> scope) {
    List<CustomObject> customObjects; //here build a list of custom object;
    insert customObjects;
    List<RetryableJob__c> futureJobs = new List<RetryableJob__c>();
    for (CustomObject customObject : customObjects) {
        Id customObjectId = customObject.Id;
        Retryable job = new RetryableCallout(customObjectId);
        futureJobs.add(new RetryableJob__c(
                status__c = Retryable.Status.FAILED_RETRY.name(),
                nextTry__c = System.now(),
                lastTry__c = System.now(),
                serializedJob__c = JSON.serialize(job),
                count__c = 0,
                message__c = 'INSERTED IN BATCH',
                className__c = String.valueOf(job).split(':')[0]
        ));
    }
    insert futureJobs;
}

And your RetryableCallout class would extend Retryable and look like this:

public class RetryableCallout extends Retryable {

private Id customObjectId;
public void RetryableCallout(Id customObjectId){
    this.customObjectId = customObjectId;
}

protected override JobResult startJob() {
    CustomObject__c customObject = [
            SELECT Id, Name, Customfield__c 
            FROM CustomObject__c 
            WHERE Id = : this.customObjectId
    ];
    HttpRequest request = new HttpRequest();
    request.setEndpoint('callout:YOUR_SERVICE_ENDPOINT');
    request.setMethod('POST');
    request.setHeader('Content-Type', 'application/json');
    request.setHeader('Accept', 'application/json');
    request.setBody('{"CustomObjectId": "' + customObjectId + '"}');
    HttpResponse response = http.send(request);
    Integer httpResponseCode = response.getStatusCode();
    switch on httpResponseCode{
        when 200,201{
            return JobResult.success(response.getBody());
        }
        when 401{
            return JobResult.actionRequired(response.getBody());
        }
        when else {
            return JobResult.retry(response.getBody());
        }
    }
}

}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .