3

I'm trying to build a general-purpose piece of functionality that can be added to batch classes. My class is Batchable itself and it will take in an instance of another Batchable. For each of the methods in the Batchable interface, my class will do some extra work, then delegate to the other Batchable. In doing so, I'm having trouble...

Something that stands out as very odd about the Batchable interface is that the docs say you must implement:

global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {}

So the return type of the start() method could be Database.QueryLocator or Iterable<sObject>. Which is not normally possible.

Most of the time, this doesn't matter, but if I have a reference to an instance of a Batchable, it seems like I can't call the Database.QueryLocator version. For example:

public with sharing class BatchableStartMethodTest implements Database.Batchable<SObject> {

    public void execute(Database.BatchableContext bc, List<SObject> scope) {
    }

    public void finish(Database.BatchableContext bc) {
    }

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return null;
    }

    public static void doesntCompile() {
        Database.Batchable<SObject> instance = new BatchableStartMethodTest();
        Database.QueryLocator locator = instance.start(null);
    }

    public static void compiles() {
        BatchableStartMethodTest instance = new BatchableStartMethodTest();
        Database.QueryLocator locator = instance.start(null);
    }

}

In this, the doesntCompile() method gives a compilation error:

Error:(20, 31) Illegal assignment from System.Iterable to Database.QueryLocator (20:31)

I can see what the error is getting at, but I can't see how to tell the compiler that I'm using the QueryLocator version. I might try to define my own derived interface, but that's not possible either:

public interface BatchableQueryLocator extends Database.Batchable<SObject> {

    Database.QueryLocator start(Database.BatchableContext bc);

}

Error:(7, 18) Method return types clash: System.Iterable vs Database.QueryLocator from the type Database.Batchable (7:18)

Does this just mean that two return types in Batchable is some weird special case in the compiler that I cannot deal with in the way I want to? I could create an interface that doesn't extend the existing Batchable i.e.

public interface BatchableQueryLocator  {

    Database.QueryLocator start(Database.BatchableContext bc);

}

But that seems ugly because then I don't get full type-safety - I can't tell the compiler that my method expects something that implements both Batchable and BatchableQueryLocator. Any suggestions?

2 Answers 2

6

I solved this problem by using Iterable<Object> as the return type. Interestingly, Database.QueryLocator implements Iterable, so it actually does work the way may not expect. Here's the full implementation:

public class BatchProcess implements Database.Stateful, Database.Batchable<Object>, Database.AllowsCallouts {
    BatchGenerators.ObjectGenerator generator;
    BatchActions.ObjectAction action;
    public BatchProcess(BatchGenerators.ObjectGenerator generator, BatchActions.ObjectAction action) {
        this.generator = generator;
        this.action = action;
    }
    public Iterable<Object> start(Database.BatchableContext context) {
        return generator.generator();
    }
    public void execute(Database.BatchableContext context, Object[] scope) {
        action.execute(scope);
    }
    public void finish(Database.BatchableContext context) {

    }
}

public class BatchActions {
    public interface ObjectAction {
        void execute(Object[] items);
    }
}

public class BatchGenerators {
    public interface ObjectGenerator {
        Iterable<Object> generator();
    }
}

To get QueryLocator to work right, you have to cast it:

public class RecordsFromQuery implements BatchGenerators.ObjectGenerator {
    String query;
    public RecordsFromQuery(String query) {
        this.query = query;
    }
    public Iterable<Object> generator() {
        return (Iterable<Object>)Database.getQueryLocator(query);
    }
}

The interfaces ObjectGenerator and ObjectAction allows a developer to specify different types of iterables or query locators. Here's an example execution:

   Database.executeBatch(
    new BatchProcess(
        new BatchGenerators.RecordsFromQuery('SELECT Id FROM Lead'),
        new BatchActions.UpdateRecords()
    )
  );

Where RecordsFromQuery just returns a dynamic Database.QueryLocator, and UpdateRecords is a simple Database.update(records, false); (this version is simply meant to try and apply triggers to existing records).

I have a more complicated framework that I'm working on, but I think this simple example should help you get where you're trying to go.

2
  • Thanks! Can't believe I missed the fact that QueryLocator implements Iterable.
    – Aidan
    Oct 15, 2018 at 12:16
  • @Aidan you'd be forgiven for not knowing that--it's not in the documentation, and I discovered it completely by accident (including the entire look-of-surprise/eureka-moment response). Plus, the batchable documentation doesn't suggest this either. Some of us just like experimenting.
    – sfdcfox
    Oct 15, 2018 at 12:30
0

Another compilation fix is to downcast the start function's return value to the appropriate type. For example:

Database.QueryLocator queryLocator = (Database.QueryLocator) batchable.start(...);

That assumes that return type will always be one or the other. One could also initially downcast to the Iterable and then do an instanceOf check for the Database.QueryLocator and then downcast to that if needed.

I recently ran into the opposite compilation error of "Illegal assignment from System.Iterable to Database.QueryLocator" when trying to create some test utility code for batchable apex with apex callouts to run the batchable manually. Downcasting to the QueryLocator fixed it for me.

You must log in to answer this question.

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