3

I implemented the following interface and class as an attempt to achieve something like a dynamic batch process that could be run in production from an anonymous APEX window.

global interface DynamicBatch {

    Database.QueryLocator dynamicQueryLocator();
    void execute(List objects);

}
global class DynamicBatchWrapper implements Database.Batchable {

    private DynamicBatch realBatch;

    global DynamicBatchWrapper(DynamicBatch realBatch) {
        this.realBatch = realBatch;
    }

    global Database.QueryLocator start(Database.BatchableContext BC) {
        return this.realBatch.dynamicQueryLocator();
    }

    global void execute(Database.BatchableContext BC, List objects) {
        this.realBatch.execute(objects);
    }

    global void finish(Database.BatchableContext BC) {
        // Unused
    }
}

My goal was to implement the real logic in an anonymous APEX window and run it like this:

class DynamicBatchImpl implements DynamicBatch {

    public Database.QueryLocator dynamicQueryLocator() {
        return Database.getQueryLocator([select Id from Order]);
    }

    public void execute(List orders) {
        // (1) Do something with each Order
        // (2) Update orders
    }

}

Database.executeBatch(
    new DynamicBatchWrapper(
        new DynamicBatchImpl()),
    10);

However, the execution ends with errors like:

Salesforce System Error: 1528894801-333603 (676567375) (676567375)

Nothing useful is printed on the logs.

I'm wondering if there is a scope issue due to the fact that I implemented DynamicBatch in the anonymous APEX window.

1 Answer 1

5

I actually just tried this myself a few days ago, because we had a similar issue-- we wanted to be able to run batches to do things like mass updating records using particular logic that we could define from execute anonymous calls. As it went, we also ran into Internal Server Errors trying to run the code. The problem stems from the fact that classes defined in an execute anonymous call aren't persisted to the database-- their metadata literally disappears before the batch class has a chance to run. In my case, I came up with a limited workaround to perform the two most usual actions: touch a group of records to allow triggers/workflows to run, and delete a set of records that match certain criteria.

Ultimately, that led me to writing the following class:

public class Batch implements Database.Batchable<Object> {
    public interface Generator {
        Object iterable(Database.BatchableContext context);
    }
    public interface Action {
        void execute(Database.BatchableContext context, Object[] scope);
    }
    public interface Finish {
        void finish(Database.BatchableContext context);
    }
    Generator gen;
    Action act;
    Finish fin;
    public Batch(Generator gen, Action act, Finish fin, Integer batchSize) {
        this.gen = gen;
        this.act = act;
        this.fin = fin;
        Database.executeBatch(this, batchSize);
    }
    public Batch(Generator gen, Action act, Finish fin) {
        this.gen = gen;
        this.act = act;
        this.fin = fin;
        Database.executeBatch(this);
    }
    public Iterable<Object> start(Database.BatchableContext context) {
        return (Iterable<Object>)gen.iterable(context);
    }
    public void execute(Database.BatchableContext context, Object[] scope) {
        act.execute(context, scope);
    }
    public void finish(Database.BatchableContext context) {
        fin.finish(context);
    }
}

Then, I had separate classes for the various things I wanted to do. Here's my Actions class:

public class Actions {
    public class UpdateRecords implements Batch.Action {
        public void execute(Database.BatchableContext context, Object[] scope) {
            Database.update((SObject[])scope, false);
        }
    }
    public class DeleteRecords implements Batch.Action {
        public void execute(Database.BatchableContext context, Object[] scope) {
            Database.delete((SObject[])scope, false);
        }
    }
}

Similarly, I wrote a default Generator:

public class Generators {
    public class FromQuery implements Database.Generator {
        String query;
        public FromQuery(String query) {
            this.query = query;
        }
        Object iterable(Database.BatchableContext context) {
            return Database.getQueryLocator(query);
        }
    }
}

The Finish methods are uninteresting, but were primarily a concept for sending an email to an administrator, doing other cleanup activity, chaining to another batch, and so on.

There's still some cleanup going on in these classes as they evolve, but I thought I'd share some stuff I've done so that it might help you out.

So, a typical use might look like this:

new Batch(new Generator.FromQuery('SELECT Id FROM Opportunity'), new Action.UpdateRecords(), new Finish.DoNothing());

A single statement to execute, and exceptionally customizable. I plan on placing this into a managed package to share, but do feel free to borrow the design for your own internal use.

You must log in to answer this question.

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