Sunday, October 20, 2013

An All-Purpose Schedulable Batch Utility

It seems like I frequently need to run a simple batch process or schedule a cleanup task and am a little put out by the amount of work required. And it's not the most creative, rewarding work, either. Create a batchable class; create a scheduleable class; unit test... Ugh. So this post is a utility class for scheduling a simple batch process without doing any of that.

First, here are a couple of usage examples. These can be run in an "Execute Anonymous" window in the developer console or Force.com IDE.

// 1. schedule a process to delete old accounts and email myself when complete
System.schedule('Account Cleanup', '0 0 0 1 * *', new SF_Batch('Delete old accounts', 
  SF_Batch.OP.OP_DELETE, 'SELECT Id FROM Account WHERE CreatedDate != THIS_YEAR', 
  new String[] {'scox67@gmail.com'}));
// 2. mark all my cases as 'TEST'
class AccountNameUpdater implements SF_Batch.Handler {
  public void handle(SObject[] scope) {
    for (Case c : (Case[])scope) {
      c.Name += ' TEST';
    }
  }
}
Database.executeBatch(new SF_Batch('Mark Test Cases', SF_Batch.OP.OP_UPDATE, 
  'SELECT Name FROM Case WHERE Owner.Name = \'Steve Cox\'', 
  new AccountNameUpdater(), null);

If several scheduled cleanup tasks are needed in your org, you can create a custom setting and use a simple function like scheduleAllJobs (below) to load them all up at once. Here's the complete code listing. Enjoy!

public class SF_Batch implements Database.Batchable<SObject>, Schedulable {
  //--------------------------------------------------------------------------
  // Constants
  public enum OP { OP_UPDATE, OP_DELETE, OP_UNDELETE }

  private final static String ALL_ROWS =  ' ALL ROWS ';
  
  // {0} = process name, {1} = status
  private final static String NOTIFY_SUBJECT = 'Salesforce Batch Process: {0} - {1}';
  // {0} = total batches processed, {1} = number of failures
  private final static String NOTIFY_BODY = '{0} batch(es) processed, with {1} failure(s).';
  
  
  //--------------------------------------------------------------------------
  // Properties
  private String name;
  private String query;
  private OP operation;
  private Handler handler;
  private String[] notify;
  
  
  //--------------------------------------------------------------------------
  // Constructor
  /**
   * Create a batch process
   * @param name optional name of the process. This will be reported in the 
   *  notification email.
   * @param operation the operation to perform
   * @param query the query used to select records
   * @param handler an optional handler. If specified, h.handle() will be called 
   *  and passed the array of objects before the operation is performed.
   * @param notify optional email addresses for finish or error notifications
   */
  public SF_Batch(String name, OP operation, String query, Handler handler, String[] notify) {
    SF.preCondition(null != operation, 'SF_Batch.SF_Batch() - missing operation');
    SF.preCondition(!SF.isEmpty(query), 'SF_Batch.SF_Batch() - missing query');
    
    this.name = name;
    this.operation = operation;
    this.handler = handler;
    this.notify = notify;
    
    if ((OP.OP_UNDELETE == operation) && !query.contains(ALL_ROWS)) {
      this.query = query + ALL_ROWS;
    } else {
      this.query = query;
    }
    
    // validate that the query is valid
    SObject[] o = Database.query(this.query);
  }
  
  public interface Handler {
    void handle(SObject[] scope);
  }
  
  /** Database.Batchable method */
  public Database.QueryLocator start(Database.BatchableContext context) {
    return Database.getQueryLocator(query);
  }
  
  /** Database.Batchable method */
  public void execute(Database.BatchableContext context, SObject[] scope) {
    if (null != handler) {
      handler.handle(scope);
    }
    
    if (OP.OP_UPDATE == operation) {
      update scope;
    } else if (OP.OP_DELETE == operation) {
      delete scope;
    } else if (OP.OP_UNDELETE == operation) {
      undelete scope;
    }
  }
  
  /** Database.Batchable method */
  public void finish(Database.BatchableContext context) {
    if (null != notify) {
      AsyncApexJob a = [SELECT Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, 
        CreatedBy.Name FROM AsyncApexJob WHERE Id = :context.getJobId()];
      
      final String subject = String.format(NOTIFY_SUBJECT, new String[]{
        String.valueOf(name), String.valueOf(a.Status)});
      final String body = String.format(NOTIFY_BODY, new String[]{
        String.valueOf(a.TotalJobItems), String.valueOf(a.NumberOfErrors)});
      
      new SF.Email(notify, null, null, subject, body).send();
    }
  }
  
  
  /** Schedulable method */
  public void execute(SchedulableContext SC) {
    Database.executeBatch(new SF_Batch(name, operation, query, handler, notify));
  }

  //--------------------------------------------------------------------------
  // Schedule/abort processes from custom settings
  /**
   * Schedule all active jobs specified in the SF Job custom setting.
   */
  public static void scheduleAllJobs() {
    final Map<String, Batch.OP> opMap = new Map<String, Batch.OP> {
      'update'   => SF_Batch.OP.OP_UPDATE,
      'delete'   => SF_Batch.OP.OP_DELETE,
      'undelete' => SF_Batch.OP.OP_UNDELETE
    };
    
    SF_Job__c[] jobs = new SF_Job__c[]{};
    for (SF_Job__c j : [SELECT Name, Schedule__c, Operation__c, Query__c, 
      Email_Recipients__c FROM SF_Job__c WHERE Active__c = true]) {
      
      final SF_Batch b = new SF_Batch(j.Name, opMap.get(j.Operation__c.toLowerCase()), 
        j.Query__c, null, (null != j.Email_Recipients__c)? j.Email_Recipients__c.split(',') : null);
        
      final Id cronId = System.schedule(j.Name, j.Schedule__c, b);
      jobs.add(new SF_Job__c(Id = j.Id, Job_Id__c = cronId));
    }
    
    update jobs;
  }
  
  /**
   * Abort all active jobs launched with scheduleAllJobs
   */
  public static void abortAllJobs() {
    SF_Job__c[] jobs = new SF_Job__c[]{};
    for (SF_Job__c j : [SELECT Job_Id__c FROM SF_Job__c WHERE Job_Id__c != null]) {
      System.abortJob(j.Job_Id__c);
      jobs.add(new SF_Job__c(Id = j.Id, Job_Id__c = null));
    }
    
    update jobs;
  }
}

No comments:

Post a Comment