7

I can't find anyone writing about this, but this works when executed anonymously:

public class foo {
    public void bar() {
        system.debug('\n\n#### FOO BAR ####\n\n');
    }
}
foo f = new foo();
f.bar();

That looks useful! Anyone taking advantage of this in clever ways already?

I want to use it to evolve Salesforce's SearchAndReplace example so I can write anonymous clean up code that takes full advantage of batching. It wouldn't let me implement Database.Batchable in my anonymous apex class so I tried this anonymous code which implements an interface and it's one method to approximate a delegate:

public class anonBatchUpdater implements BatchUtil.BatchUtilable {
    public boolean HandleRecord(Sobject s, BatchUtil bu) {
        // this is basically a delegate in my anonymous APEX where I can do things
        // more complicated than allowed by SearchAndReplace(q,e,f,v) like:
        // s.put('field1', s.get('field2') + s.get('field3'));

        // for now let's just see if we get here...
        bu.Log('Saw record ' + s.Id);
        return false;
    }
}
integer BATCH_SIZE = 100;
string SOQL = 'select company from lead limit 3';
Database.executeBatch(new BatchUtil(SOQL, new anonBatchUpdater()), BATCH_SIZE);

Backed by this real class:

// Allows for doing quick batch updates in Anonymous Apex
global class BatchUtil implements Database.Batchable<sObject>, Database.Stateful {
    global final String Query;
    global final BatchUtilable Updater;
    private String RunningLog; // depends on Database.Stateful

    global interface BatchUtilable {
        boolean HandleRecord(Sobject s, BatchUtil bu);
    }

    global void Log(string log) {
        RunningLog += log;
    }

    global BatchUtil(String q, BatchUtilable upd) {
        Query = q;
        Updater = upd;
        RunningLog = 'Started BatchUtil...\n';
    }

    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC, List<sObject> scope) {
        List<sObject> ToCommit = new List<Sobject>();
        for(Sobject s : scope) {
            if (updater.HandleRecord(s, this)) {
                ToCommit.add(s);
            }
        }
        if (ToCommit.size() > 0)
            database.update(ToCommit, false);
            // TODO: log saveresults
    }

    global void finish(Database.BatchableContext BC) {
        system.debug(LoggingLevel.INFO, '\n\n\n\n################\nSTART BATCHUTIL LOG:\n\n' + runningLog + '\n\nEND BATCHUTIL LOG\n################\n');
    }
}

My tests pass fine:

public class BatchUtilTest implements BatchUtil.BatchUtilable {
    public boolean HandleRecord(Sobject s, BatchUtil bu) {
        string log = '';
        if (s.get('LastName') == 'testing1') {
            s.put('Company', 'updated');
            bu.Log('Found ' + s.get('LastName') + ' and updated company to: "' + s.get('Company') + '"\n'); 
            return true; // we want to update this record
        } else {
            bu.Log('Ignoring ' + s.get('LastName'));
            return false; // skip this record
        }
    }

    @isTest
    static void Test() {
        Lead l1 = new Lead();
        l1.LastName = 'testing1';
        l1.Company = 'testing1';
        insert l1; // pull immediately to get LastModifiedDate for testing later
        l1 = [select LastName, Company, LastModifiedDate from Lead where Id = :l1.Id];
        Lead l2 = new Lead();
        l2.LastName = 'testing2';
        l2.Company = 'testing2';
        insert l2; // pull immediately to get LastModifiedDate for testing later
        l2 = [select LastName, Company, LastModifiedDate from Lead where Id = :l2.Id];

        Test.startTest();
        Database.executeBatch(new BatchUtil('select lastname, company from lead', new BatchUtilTest()), 10);
        Test.stopTest();

        Lead l1refreshed = [select LastName, Company, LastModifiedDate from Lead where Id = :l1.Id];
        system.assertEquals('updated', l1refreshed.Company);
        system.assertNotEquals(l1.LastModifiedDate, l1refreshed.LastModifiedDate);
        Lead l2refreshed = [select LastName, Company, LastModifiedDate from Lead where Id = :l2.Id];
        system.assertEquals('testing2', l2refreshed.Company);
        system.assertEquals(l2.LastModifiedDate, l2refreshed.LastModifiedDate);
    }
}

And give output like this:

START BATCHUTIL LOG:

Started BatchUtil...
Found testing1 and updated company to: "updated"
Ignoring testing2

END BATCHUTIL LOG

But when I try from anonymous apex the job just sits in Apex Jobs with a Status of Queued and I finally dug this out of the logs:

FATAL_ERROR|Internal Salesforce.com Error

Have I pushed anonymous apex classes too far? Anything I can do to make my code work? Is there an idea or two on the ideaexchange that would make this code work? Anyone have a similar utility already?

1
  • 1
    An interesting idea. If I had to guess (and I do), I'd say it is falling over with trying to pass the anonymous class off to the application servers. When the application server picks the batch job off the async queue it probably can't get the anonymous class definition. Commented Apr 3, 2013 at 23:36

1 Answer 1

6

I just write standard classes to do clean up, but then trigger the batch with a one liner if I want to.

I suspect the reason you're hitting this error is because during tests you can force everything to execute within a single context, wheras when you fire the batch using execute anonymous directly, the batch itself will run in a different context to your initial request. The upshot of that being that the class anonBatchUpdater won't actually exist any more which makes it pretty hard to use its methods!

5
  • Sorry for being dense, but by "I just write standard classes to do clean up" you mean that you write a temporary APEX class, minimal test case, publish it out to production, execute it once, then delete it in production and on dev? I could see that being worth in a larger org. So far I've just simulated clean up batching with anonymous apex run, wait, run, wait, repeat until done. That of course depends on SOQL that ignores the handled records with a limit on it or manually moving the SOQL over a range to make manageable size record sets for each iteration. Appreciate the answer.
    – twamley
    Commented Apr 4, 2013 at 4:19
  • @twamley I just mean I write a regular apex class and leave it there, so it's always ready for use! If you really need to lock it down tight you could make part of the start() method a check on the current username and/or email. If it's not a match then return 0 records.
    – Matt Lacey
    Commented Apr 4, 2013 at 5:02
  • Gotcha. I keep running into different one-time clean up scenarios that are never needed again. Sounds like I should keep a batchable utility class laying around and just upgrade the SOQL and the record action each time I need it for something else. Publish it. Run it. Leave it dormant until next time. Thanks.
    – twamley
    Commented Apr 4, 2013 at 5:54
  • You could use a string property in the class which represents the query, then to run it you could create an instance, set the string and then fire it. The execute method would just have to be delete scope;
    – Matt Lacey
    Commented Apr 4, 2013 at 22:01
  • Salesforce Support liked your explanation too. Agree with your delete example. A majority of my "cleanup" is really fix up a set of fields based on another set of fields so I need SOQL, a for loop, a couple lines of code that execute against every row, and a final update. That's why I thought my code attempt was clever :) Too bad they can't persist the class definition and reuse it on the application servers. Thanks for the answers.
    – twamley
    Commented Apr 6, 2013 at 16:36

You must log in to answer this question.

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