function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Dan NorfleetDan Norfleet 

Batch Apex Question

This is my first Apex Class.  I have tried to imlement the Batchable Class given by SalesForce and have the current logic working for a set of records when i execute from Developer's Console using the following:
     Batch_OrderStatus b = new Batch_OrderStatus();
     database.executebatch(b);

Records are updated successfully.  However, will only execute the limit i enter in the query.  I want the code to execute over and over until all records within the Select driver are exhausted.   It has been suggested to move the two Select Count queries into a map and not have them within the FOR loop.  I will try to make that change, but wanted to get clarification on a couple other questions.  I appreciate any direction the community can give me.

1)  What is best way to schedule a  batch process that is going to run daily?  Should I use the Schedule option within SalesForce within Apex Classes?  Add to batch job table?  Or is there a better way?
2)  How can I change my logic so it executes over and over until the initial query has been completed without getting Limit exceptions?  I know there is a better way to do this, but the current code just pulls the first 25 records out of the 300 that are returned in the query.

Following is my current code:

/*
 * @Name: Batch_OrderStatus
 * @Author: Dan Norfleet
 * Updates : February 2018 - Dan Norfleet - Created Apex Class
 */
global with sharing class Batch_OrderStatus implements Database.Batchable<sObject> {
    global final String query;
    public String statusSubmitted = 'Order Submitted';
    public String emailText = '\n ACTION           ORDER                  STATUS\n ------------------------------------------------';
    public List<ccrz__E_Order__c> ordList = new List<ccrz__E_Order__c>();
    public Integer total_orders_cnt = 0;
    public Integer update_received_cnt = 0;
    public Integer update_cancelled_cnt = 0;
    public String status_received = 'Received';
    public String status_order_received = 'Order Received';

    global Database.QueryLocator start(Database.BatchableContext BC) {
        String query = 'SELECT Id, name, ccrz__orderstatus__c FROM ccrz__E_Order__c WHERE ccrz__orderstatus__c = \'Order Submitted\'';
        query = query + 'limit 25';
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC, List<sObject> scope) {
          // Loop through the list of orders
          for(sobject s : scope)
          { 
            ccrz__E_Order__c ord = (ccrz__E_Order__c)s;
            total_orders_cnt = total_orders_cnt + 1;
            String order_id = ord.Id;
            // For each order, see how many items are in 'Received' status
            integer ItemsReceived = 
                    [SELECT COUNT() 
                    FROM ccrz__E_OrderItem__c 
                    WHERE ccrz__orderitemstatus__c = :status_received
                    AND ccrz__order__c = :order_id];
              
            integer ItemsNOTReceived = 
                    [SELECT COUNT() 
                    FROM ccrz__E_OrderItem__c 
                    WHERE ccrz__orderitemstatus__c != :status_received
                    AND ccrz__order__c = :order_id];
          
            if (ItemsReceived > 0 && ItemsNOTReceived == 0)
                { 
                ord.ccrz__orderstatus__c = 'Order Received';
                update_received_cnt = update_received_cnt + 1;
                emailText = emailText + '\n Updated         ' + ord.Name + '      Order Received';
                }
            else if (ItemsReceived == 0 && ItemsNOTReceived == 0)
                { 
                ord.ccrz__orderstatus__c = 'Cancelled';
                update_cancelled_cnt = update_cancelled_cnt + 1;
                emailText = emailText + '\n Updated         ' + ord.Name + '      Cancelled';
                }
                        else
                                {
                emailText = emailText + '\n No Change     ' + ord.Name + '      Order Submitted';
                                }

          }
          update scope;
          
                  /* E-Mail */
                  //Send an email to the User after your batch completes
    System.debug('    emailText = ' + emailText);
                  Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                  mail.ToAddresses  = new string[] {'batch.example@google.com'};
                  mail.setSubject('Apex Batch Job is done');
                  mail.setPlainTextBody(emailText);
                  Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }

    global void finish(Database.BatchableContext BC) {
          AsyncApexJob a = [SELECT Id,
                            Status,
                            NumberOfErrors,
                            JobItemsProcessed,
                            TotalJobItems,
                            CompletedDate,
                            ExtendedStatus,
                            ApexClass.name,
                            CreatedBy.Email,
                            CreatedBy.Name
                            FROM AsyncApexJob
                            WHERE Id = :BC.getJobId()];
  
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            mail.ToAddresses  = new string[] {a.CreatedBy.Email};
            mail.setSubject('Async Apex Batch Job Status' + a.ApexClass.name);
    
            string td1 = '"border:1px solid #e6e6e6; width=200px;"';
            string td2 = '"width=200px; border:1px solid #e6e6e6; background-color:#E44905; color:white; font-weight:bold;"';
            string tdHead = '"border:1px solid #e6e6e6; width=200px; color:white; background-color:#9370DB; font-weight:bold;"';
            string ExtendedStatus = '';
            if (a.ExtendedStatus != null)
                ExtendedStatus = a.ExtendedStatus;
            string tdErrors = td1;
            if (a.NumberOfErrors > 0)
                tdErrors = td2;
            string htmlBody = '<div style="border:2px solid #e6e6e6; border-radius:10px; "><p>Hi Team,</p><p><span style="color:brown; font-weight:bolder;">Hallmark Salesforce</span> completed running <b>Apex Batch Code.</p>'
                              + '<p>Results:</p>'
                              + '<center><table style="border:3px solid #e6e6e6; border-collapse:collapse;">'
                              + '<tr><td style=' + tdHead + '>Class Name</td><td style=' + tdHead + '>' + a.ApexClass.name + '</td></tr>'
                              + '<tr><td style=' + td1 + '>Completed Date</td><td style=' + td1 + '>' + a.CompletedDate + '</td></tr>'
                              + '<tr><td style=' + td1 + '>Status</td><td style=' + td1 + '>' + a.Status + '</td></tr>'
                              + '<tr><td style=' + td1 + '>Job Items Processed</td><td style=' + td1 + '>' + a.JobItemsProcessed + ' / ' + a.TotalJobItems + '</td></tr>'
                              + '<tr><td style=' + td1 + '>NumberOfErrors</td><td style=' + tdErrors + '>' + a.NumberOfErrors + '</td></tr>'
                              + '<tr><td style=' + td1 + '>Extended Status</td><td style=' + td1 + '>' + ExtendedStatus + '</td></tr>'
                              + '<tr><td style=' + tdHead + '>Created By</td><td style=' + tdHead + '>' + a.CreatedBy.Name + ' (' + a.CreatedBy.Email + ')</td></tr>'
                              + '</table></center>'
                              + '<p><span style="font-family:"Courier New", Courier, monospace; color:#e6e6e6; font-weight:bold; font-size:larger;">Hallmark Salesforce Admin Team</span></p></div>';
    
            mail.setHtmlBody(htmlBody);
            List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
            mails.add(mail);
            Messaging.sendEmail(mails);
    }
}
Best Answer chosen by Dan Norfleet
jigarshahjigarshah
Dan,

Here are the answers to your questions.

1)  What is best way to schedule a batch process that is going to run daily?  Should I use the Schedule option within SalesForce within Apex Classes?  Add to batch job table?  Or is there a better way?

The recommended way to schedule a job to run daily is to implement the Schedulable Interface (https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_scheduler.htm) provided by the platform. This interface enables the class implementing this to be scheduled in 2 ways
  • Via the out of box Schedule Interface which can be leveraged by navigating to Setup > Develop > Apex Classes > Select the Class Name to be schedule > Schedule Apex button > Provide Job, Class Name and Schedule for automatic execution. This technique generates a cron expression internally that is leveraged for scheduling. The limitation by scheduling through this approach is that you cannot schedule jobs to run in successive time intervals of less than an hour i.e. say you intend to schedule a job every 5 seconds for listening to a new Order creation within Salesforce by an external system and process it. 
 
  • The limitation in # 1 can be addressed by scheduling a job through code as shown in the sample code below
scheduledMerge m = new scheduledMerge();
String sch = '20 30 8 10 2 ?';  //Custom Cron Expression
String jobID = system.schedule('Your Job Name', sch, m);
global class scheduledBatchable implements Schedulable {
   global void execute(SchedulableContext sc) {
      Batch_OrderStatus b = new Batch_OrderStatus();
      database.executebatch(b);
   }
}
  • You can then use the Scheduled Jobs console to monitor the scheduled Jobs
  • Since the same class can implement multiple interfaces, you can have the same class implement the Schedulable interface

2)  How can I change my logic so it executes over and over until the initial query has been completed without getting Limit exceptions?  I know there is a better way to do this, but the current code just pulls the first 25 records out of the 300 that are returned in the query.

Batch jobs are designed for bullk processign and can process upto 5 million records. By default the batch size is set to 200 records but it can be set to suit your needs and can be extended to process a maximum of 2000 records per batch. i.e. if you have a total of 1000 records to be processed in a single Batch Job execution, the batch job processes 5 batches of 200 records per batch to complete. 

This only determines the number of records that process in a single batch of the complete batch job execution and has no impact on the total number of records that can be processed as long as they are 5 million records.

You can simply use the overloaded version of database.executeBatch to do so as follows.
Batch_OrderStatus b = new Batch_OrderStatus();
database.executebatch(b, 500);

Moreover, I see that you have included the email sending logic in the finish method and can be simplified using this technique.
  • Create a custom object called Batch Job Log
  • In the finish method write an insert logic to create a Batch Job Log record with the information required for sending an email.
  • Write a custom workflow action on Batch Job Log, that sends an email every time a Batch Job Log record is created. 
  • Create a custom email template that the workflow rule can use to send the email.
This is a cleaner approach, reduces code and seperates the actions.

All Answers

jigarshahjigarshah
Dan,

Here are the answers to your questions.

1)  What is best way to schedule a batch process that is going to run daily?  Should I use the Schedule option within SalesForce within Apex Classes?  Add to batch job table?  Or is there a better way?

The recommended way to schedule a job to run daily is to implement the Schedulable Interface (https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_scheduler.htm) provided by the platform. This interface enables the class implementing this to be scheduled in 2 ways
  • Via the out of box Schedule Interface which can be leveraged by navigating to Setup > Develop > Apex Classes > Select the Class Name to be schedule > Schedule Apex button > Provide Job, Class Name and Schedule for automatic execution. This technique generates a cron expression internally that is leveraged for scheduling. The limitation by scheduling through this approach is that you cannot schedule jobs to run in successive time intervals of less than an hour i.e. say you intend to schedule a job every 5 seconds for listening to a new Order creation within Salesforce by an external system and process it. 
 
  • The limitation in # 1 can be addressed by scheduling a job through code as shown in the sample code below
scheduledMerge m = new scheduledMerge();
String sch = '20 30 8 10 2 ?';  //Custom Cron Expression
String jobID = system.schedule('Your Job Name', sch, m);
global class scheduledBatchable implements Schedulable {
   global void execute(SchedulableContext sc) {
      Batch_OrderStatus b = new Batch_OrderStatus();
      database.executebatch(b);
   }
}
  • You can then use the Scheduled Jobs console to monitor the scheduled Jobs
  • Since the same class can implement multiple interfaces, you can have the same class implement the Schedulable interface

2)  How can I change my logic so it executes over and over until the initial query has been completed without getting Limit exceptions?  I know there is a better way to do this, but the current code just pulls the first 25 records out of the 300 that are returned in the query.

Batch jobs are designed for bullk processign and can process upto 5 million records. By default the batch size is set to 200 records but it can be set to suit your needs and can be extended to process a maximum of 2000 records per batch. i.e. if you have a total of 1000 records to be processed in a single Batch Job execution, the batch job processes 5 batches of 200 records per batch to complete. 

This only determines the number of records that process in a single batch of the complete batch job execution and has no impact on the total number of records that can be processed as long as they are 5 million records.

You can simply use the overloaded version of database.executeBatch to do so as follows.
Batch_OrderStatus b = new Batch_OrderStatus();
database.executebatch(b, 500);

Moreover, I see that you have included the email sending logic in the finish method and can be simplified using this technique.
  • Create a custom object called Batch Job Log
  • In the finish method write an insert logic to create a Batch Job Log record with the information required for sending an email.
  • Write a custom workflow action on Batch Job Log, that sends an email every time a Batch Job Log record is created. 
  • Create a custom email template that the workflow rule can use to send the email.
This is a cleaner approach, reduces code and seperates the actions.
This was selected as the best answer
Dan NorfleetDan Norfleet

Jigarshah,

Thank you so much for the detail on my 2 questions.  This is extremly helpful!

Dan
jigarshahjigarshah
Dan,

I am glad, that I was able to help. Can you please close this thread if your query has been addressed?