[SalesForce] Is it possible to call Database.QueryLocator start() when you only have a reference to the interface

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?

Best Answer

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.

Related Topic